Merge branch 'dev' into test/e2e-setup
This commit is contained in:
commit
2710e1354e
37 changed files with 11109 additions and 600 deletions
|
@ -66,7 +66,7 @@ export async function putAssignmentHandler(req: Request, res: Response): Promise
|
|||
res.json({ assignment });
|
||||
}
|
||||
|
||||
export async function deleteAssignmentHandler(req: Request, _res: Response): Promise<void> {
|
||||
export async function deleteAssignmentHandler(req: Request, res: Response): Promise<void> {
|
||||
const id = Number(req.params.id);
|
||||
const classid = req.params.classid;
|
||||
requireFields({ id, classid });
|
||||
|
@ -75,7 +75,8 @@ export async function deleteAssignmentHandler(req: Request, _res: Response): Pro
|
|||
throw new BadRequestException('Assignment id should be a number');
|
||||
}
|
||||
|
||||
await deleteAssignment(classid, id);
|
||||
const assignment = await deleteAssignment(classid, id);
|
||||
res.json({ assignment });
|
||||
}
|
||||
|
||||
export async function getAssignmentsSubmissionsHandler(req: Request, res: Response): Promise<void> {
|
||||
|
|
|
@ -3,8 +3,6 @@ import { createGroup, deleteGroup, getAllGroups, getGroup, getGroupSubmissions,
|
|||
import { GroupDTO } from '@dwengo-1/common/interfaces/group';
|
||||
import { requireFields } from './error-helper.js';
|
||||
import { BadRequestException } from '../exceptions/bad-request-exception.js';
|
||||
import { EntityDTO } from '@mikro-orm/core';
|
||||
import { Group } from '../entities/assignments/group.entity.js';
|
||||
|
||||
function checkGroupFields(classId: string, assignmentId: number, groupId: number): void {
|
||||
requireFields({ classId, assignmentId, groupId });
|
||||
|
@ -35,7 +33,11 @@ export async function putGroupHandler(req: Request, res: Response): Promise<void
|
|||
const groupId = parseInt(req.params.groupid);
|
||||
checkGroupFields(classId, assignmentId, groupId);
|
||||
|
||||
const group = await putGroup(classId, assignmentId, groupId, req.body as Partial<EntityDTO<Group>>);
|
||||
// Only members field can be changed
|
||||
const members = req.body.members;
|
||||
requireFields({ members });
|
||||
|
||||
const group = await putGroup(classId, assignmentId, groupId, { members } as Partial<GroupDTO>);
|
||||
|
||||
res.json({ group });
|
||||
}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { Collection, Entity, Enum, ManyToOne, OneToMany, PrimaryKey, Property } from '@mikro-orm/core';
|
||||
import { Cascade, Collection, Entity, Enum, ManyToOne, OneToMany, PrimaryKey, Property } from '@mikro-orm/core';
|
||||
import { Class } from '../classes/class.entity.js';
|
||||
import { Group } from './group.entity.js';
|
||||
import { Language } from '@dwengo-1/common/util/language';
|
||||
|
@ -34,6 +34,7 @@ export class Assignment {
|
|||
@OneToMany({
|
||||
entity: () => Group,
|
||||
mappedBy: 'assignment',
|
||||
cascade: [Cascade.ALL],
|
||||
})
|
||||
groups: Collection<Group> = new Collection<Group>(this);
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { Student } from '../users/student.entity.js';
|
||||
import { Group } from './group.entity.js';
|
||||
import { Entity, Enum, ManyToOne, PrimaryKey, Property } from '@mikro-orm/core';
|
||||
import { Entity, Enum, ManyToOne, PrimaryKey, Property, Cascade } from '@mikro-orm/core';
|
||||
import { SubmissionRepository } from '../../data/assignments/submission-repository.js';
|
||||
import { Language } from '@dwengo-1/common/util/language';
|
||||
|
||||
|
@ -21,8 +21,8 @@ export class Submission {
|
|||
@PrimaryKey({ type: 'numeric', autoincrement: false })
|
||||
learningObjectVersion = 1;
|
||||
|
||||
@ManyToOne({
|
||||
entity: () => Group,
|
||||
@ManyToOne(() => Group, {
|
||||
cascade: [Cascade.REMOVE],
|
||||
})
|
||||
onBehalfOf!: Group;
|
||||
|
||||
|
|
|
@ -7,8 +7,17 @@ import { GroupDTO, GroupDTOId } from '@dwengo-1/common/interfaces/group';
|
|||
import { SubmissionDTO, SubmissionDTOId } from '@dwengo-1/common/interfaces/submission';
|
||||
import { fetchAssignment } from './assignments.js';
|
||||
import { NotFoundException } from '../exceptions/not-found-exception.js';
|
||||
import { putObject } from './service-helper.js';
|
||||
import { fetchStudents } from './students.js';
|
||||
import { fetchClass } from './classes.js';
|
||||
import { BadRequestException } from '../exceptions/bad-request-exception.js';
|
||||
import { Student } from '../entities/users/student.entity.js';
|
||||
import { Class } from '../entities/classes/class.entity.js';
|
||||
|
||||
async function assertMembersInClass(members: Student[], cls: Class): Promise<void> {
|
||||
if (!members.every((student) => cls.students.contains(student))) {
|
||||
throw new BadRequestException('Student does not belong to class');
|
||||
}
|
||||
}
|
||||
|
||||
export async function fetchGroup(classId: string, assignmentNumber: number, groupNumber: number): Promise<Group> {
|
||||
const assignment = await fetchAssignment(classId, assignmentNumber);
|
||||
|
@ -28,15 +37,18 @@ export async function getGroup(classId: string, assignmentNumber: number, groupN
|
|||
return mapToGroupDTO(group, group.assignment.within);
|
||||
}
|
||||
|
||||
export async function putGroup(
|
||||
classId: string,
|
||||
assignmentNumber: number,
|
||||
groupNumber: number,
|
||||
groupData: Partial<EntityDTO<Group>>
|
||||
): Promise<GroupDTO> {
|
||||
export async function putGroup(classId: string, assignmentNumber: number, groupNumber: number, groupData: Partial<GroupDTO>): Promise<GroupDTO> {
|
||||
const group = await fetchGroup(classId, assignmentNumber, groupNumber);
|
||||
|
||||
await putObject<Group>(group, groupData, getGroupRepository());
|
||||
const memberUsernames = groupData.members as string[];
|
||||
const members = await fetchStudents(memberUsernames);
|
||||
|
||||
const cls = await fetchClass(classId);
|
||||
await assertMembersInClass(members, cls);
|
||||
|
||||
const groupRepository = getGroupRepository();
|
||||
groupRepository.assign(group, { members } as Partial<EntityDTO<Group>>);
|
||||
await groupRepository.getEntityManager().persistAndFlush(group);
|
||||
|
||||
return mapToGroupDTO(group, group.assignment.within);
|
||||
}
|
||||
|
@ -63,6 +75,9 @@ export async function createGroup(groupData: GroupDTO, classid: string, assignme
|
|||
const memberUsernames = (groupData.members as string[]) || [];
|
||||
const members = await fetchStudents(memberUsernames);
|
||||
|
||||
const cls = await fetchClass(classid);
|
||||
await assertMembersInClass(members, cls);
|
||||
|
||||
const assignment = await fetchAssignment(classid, assignmentNumber);
|
||||
|
||||
const groupRepository = getGroupRepository();
|
||||
|
|
|
@ -17,6 +17,7 @@ import { ClassRepository } from '../../../src/data/classes/class-repository';
|
|||
import { Submission } from '../../../src/entities/assignments/submission.entity';
|
||||
import { Class } from '../../../src/entities/classes/class.entity';
|
||||
import { Assignment } from '../../../src/entities/assignments/assignment.entity';
|
||||
import { testLearningObject01 } from '../../test_assets/content/learning-objects.testdata';
|
||||
|
||||
describe('SubmissionRepository', () => {
|
||||
let submissionRepository: SubmissionRepository;
|
||||
|
@ -106,7 +107,7 @@ describe('SubmissionRepository', () => {
|
|||
});
|
||||
|
||||
it('should not find a deleted submission', async () => {
|
||||
const id = new LearningObjectIdentifier('id01', Language.English, 1);
|
||||
const id = new LearningObjectIdentifier(testLearningObject01.hruid, testLearningObject01.language, testLearningObject01.version);
|
||||
await submissionRepository.deleteSubmissionByLearningObjectAndSubmissionNumber(id, 1);
|
||||
|
||||
const submission = await submissionRepository.findSubmissionByLearningObjectAndSubmissionNumber(id, 1);
|
||||
|
|
|
@ -61,7 +61,7 @@ export function makeTestGroups(em: EntityManager, students: Student[], assignmen
|
|||
*/
|
||||
group1ConditionalLearningPath = em.create(Group, {
|
||||
assignment: getConditionalPathAssignment(),
|
||||
groupNumber: 1,
|
||||
groupNumber: 21005,
|
||||
members: [getTestleerling1()],
|
||||
});
|
||||
|
||||
|
|
|
@ -18,6 +18,7 @@
|
|||
"dependencies": {
|
||||
"@tanstack/react-query": "^5.69.0",
|
||||
"@tanstack/vue-query": "^5.69.0",
|
||||
"@vueuse/core": "^13.1.0",
|
||||
"axios": "^1.8.2",
|
||||
"oidc-client-ts": "^3.1.0",
|
||||
"uuid": "^11.1.0",
|
||||
|
|
|
@ -3,6 +3,9 @@
|
|||
import MenuBar from "@/components/MenuBar.vue";
|
||||
import { useRoute } from "vue-router";
|
||||
import { computed } from "vue";
|
||||
import authService from "@/services/auth/auth-service.ts";
|
||||
|
||||
void authService.loadUser();
|
||||
|
||||
const route = useRoute();
|
||||
interface RouteMeta {
|
||||
|
|
49
frontend/src/assets/assignment.css
Normal file
49
frontend/src/assets/assignment.css
Normal file
|
@ -0,0 +1,49 @@
|
|||
.container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
padding: 2%;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.assignment-card {
|
||||
width: 80%;
|
||||
padding: 2%;
|
||||
border-radius: 12px;
|
||||
}
|
||||
|
||||
.description {
|
||||
margin-top: 2%;
|
||||
line-height: 1.6;
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
.top-right-btn {
|
||||
position: absolute;
|
||||
right: 2%;
|
||||
color: red;
|
||||
}
|
||||
|
||||
.group-section {
|
||||
margin-top: 2rem;
|
||||
}
|
||||
|
||||
.group-section h3 {
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.group-section ul {
|
||||
padding-left: 1.2rem;
|
||||
list-style-type: disc;
|
||||
}
|
||||
|
||||
.subtitle-section {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.assignmentTopTitle {
|
||||
white-space: normal;
|
||||
word-break: break-word;
|
||||
}
|
|
@ -11,7 +11,7 @@
|
|||
selectedAge: { type: String, required: true },
|
||||
});
|
||||
|
||||
const { locale } = useI18n();
|
||||
const { t, locale } = useI18n();
|
||||
const language = computed(() => locale.value);
|
||||
|
||||
const { data: allThemes, isLoading, error } = useThemeQuery(language);
|
||||
|
@ -74,6 +74,22 @@
|
|||
class="fill-height"
|
||||
/>
|
||||
</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-container>
|
||||
</template>
|
||||
|
|
|
@ -11,7 +11,7 @@
|
|||
const { t, locale } = useI18n();
|
||||
|
||||
const role = auth.authState.activeRole;
|
||||
const router = useRouter();
|
||||
const _router = useRouter(); // Zonder '_' gaf dit een linter error voor unused variable
|
||||
|
||||
const name: string = auth.authState.user!.profile.name!;
|
||||
const initials: string = name
|
||||
|
|
|
@ -1,21 +1,26 @@
|
|||
<script setup lang="ts">
|
||||
import { computed } from "vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
defineProps<{
|
||||
const props = defineProps<{
|
||||
path: string;
|
||||
isAbsolutePath?: boolean;
|
||||
title: string;
|
||||
description: string;
|
||||
image: string;
|
||||
image?: string;
|
||||
icon?: string;
|
||||
}>();
|
||||
|
||||
const routerLink = computed(() => (props.isAbsolutePath ? props.path : `/theme/${props.path}`));
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<v-card
|
||||
variant="outlined"
|
||||
class="theme-card d-flex flex-column"
|
||||
:to="`theme/${path}`"
|
||||
:to="routerLink"
|
||||
link
|
||||
>
|
||||
<v-card-title class="title-container">
|
||||
|
@ -27,12 +32,18 @@
|
|||
contain
|
||||
class="title-image"
|
||||
></v-img>
|
||||
<v-icon
|
||||
v-if="icon"
|
||||
class="title-image"
|
||||
>{{ icon }}</v-icon
|
||||
>
|
||||
|
||||
<span class="title">{{ title }}</span>
|
||||
</v-card-title>
|
||||
<v-card-text class="description flex-grow-1">{{ description }}</v-card-text>
|
||||
<v-card-actions>
|
||||
<v-btn
|
||||
:to="`theme/${path}`"
|
||||
:to="routerLink"
|
||||
variant="text"
|
||||
>
|
||||
{{ t("read-more") }}
|
||||
|
|
49
frontend/src/components/assignments/DeadlineSelector.vue
Normal file
49
frontend/src/components/assignments/DeadlineSelector.vue
Normal file
|
@ -0,0 +1,49 @@
|
|||
<script setup lang="ts">
|
||||
import { ref, computed } from "vue";
|
||||
import { deadlineRules } from "@/utils/assignment-rules.ts";
|
||||
|
||||
const date = ref("");
|
||||
const time = ref("23:59");
|
||||
const emit = defineEmits(["update:deadline"]);
|
||||
|
||||
const formattedDeadline = computed(() => {
|
||||
if (!date.value || !time.value) return "";
|
||||
return `${date.value} ${time.value}`;
|
||||
});
|
||||
|
||||
function updateDeadline(): void {
|
||||
if (date.value && time.value) {
|
||||
emit("update:deadline", formattedDeadline.value);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<v-card-text>
|
||||
<v-text-field
|
||||
v-model="date"
|
||||
label="Select Deadline Date"
|
||||
type="date"
|
||||
variant="outlined"
|
||||
density="compact"
|
||||
:rules="deadlineRules"
|
||||
required
|
||||
@update:modelValue="updateDeadline"
|
||||
></v-text-field>
|
||||
</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>
|
||||
|
||||
<style scoped></style>
|
75
frontend/src/components/assignments/GroupSelector.vue
Normal file
75
frontend/src/components/assignments/GroupSelector.vue
Normal file
|
@ -0,0 +1,75 @@
|
|||
<script setup lang="ts">
|
||||
import { ref } from "vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import UsingQueryResult from "@/components/UsingQueryResult.vue";
|
||||
import type { StudentsResponse } from "@/controllers/students.ts";
|
||||
import { useClassStudentsQuery } from "@/queries/classes.ts";
|
||||
|
||||
const props = defineProps<{
|
||||
classId: string | undefined;
|
||||
groups: string[][];
|
||||
}>();
|
||||
const emit = defineEmits(["groupCreated"]);
|
||||
const { t } = useI18n();
|
||||
|
||||
const selectedStudents = ref([]);
|
||||
|
||||
const studentQueryResult = useClassStudentsQuery(() => props.classId, true);
|
||||
|
||||
function filterStudents(data: StudentsResponse): { title: string; value: string }[] {
|
||||
const students = data.students;
|
||||
const studentsInGroups = props.groups.flat();
|
||||
|
||||
return students
|
||||
?.map((st) => ({
|
||||
title: `${st.firstName} ${st.lastName}`,
|
||||
value: st.username,
|
||||
}))
|
||||
.filter((student) => !studentsInGroups.includes(student.value));
|
||||
}
|
||||
|
||||
function createGroup(): void {
|
||||
if (selectedStudents.value.length) {
|
||||
// Extract only usernames (student.value)
|
||||
const usernames = selectedStudents.value.map((student) => student.value);
|
||||
emit("groupCreated", usernames);
|
||||
selectedStudents.value = []; // Reset selection after creating group
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<using-query-result
|
||||
:query-result="studentQueryResult"
|
||||
v-slot="{ data }: { data: StudentsResponse }"
|
||||
>
|
||||
<h3>{{ t("create-groups") }}</h3>
|
||||
<v-card-text>
|
||||
<v-combobox
|
||||
v-model="selectedStudents"
|
||||
:items="filterStudents(data)"
|
||||
item-title="title"
|
||||
item-value="value"
|
||||
:label="t('choose-students')"
|
||||
variant="outlined"
|
||||
clearable
|
||||
multiple
|
||||
hide-details
|
||||
density="compact"
|
||||
chips
|
||||
append-inner-icon="mdi-magnify"
|
||||
></v-combobox>
|
||||
|
||||
<v-btn
|
||||
@click="createGroup"
|
||||
color="primary"
|
||||
class="mt-2"
|
||||
size="small"
|
||||
>
|
||||
{{ t("create-group") }}
|
||||
</v-btn>
|
||||
</v-card-text>
|
||||
</using-query-result>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
|
@ -30,4 +30,10 @@ export class LearningPathController extends BaseController {
|
|||
const dtos = await this.get<LearningPathDTO[]>("/", { theme });
|
||||
return dtos.map((dto) => LearningPath.fromDTO(dto));
|
||||
}
|
||||
|
||||
async getAllLearningPaths(language: string | null = null): Promise<LearningPath[]> {
|
||||
const query = language ? { language } : undefined;
|
||||
const dtos = await this.get<LearningPathDTO[]>("/", query);
|
||||
return dtos.map((dto) => LearningPath.fromDTO(dto));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -57,8 +57,21 @@
|
|||
"legendNotCompletedYet": "Noch nicht fertig",
|
||||
"legendCompleted": "Fertig",
|
||||
"legendTeacherExclusive": "Information für Lehrkräfte",
|
||||
"new-assignment": "Neue Aufgabe",
|
||||
"edit-assignment": "Zuordnung bearbeiten",
|
||||
"groups": "Gruppen",
|
||||
"learning-path": "Lernpfad",
|
||||
"choose-lp": "Einen lernpfad auswählen",
|
||||
"choose-classes": "Klassen wählen",
|
||||
"create-groups": "Gruppen erstellen",
|
||||
"title": "Titel",
|
||||
"pick-class": "Wählen Sie eine klasse",
|
||||
"choose-students": "Studenten auswählen",
|
||||
"create-group": "Gruppe erstellen",
|
||||
"class": "klasse",
|
||||
"delete": "löschen",
|
||||
"view-assignment": "Auftrag anzeigen",
|
||||
"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.",
|
||||
|
@ -89,6 +102,11 @@
|
|||
"noSubmissionsYet": "Noch keine Lösungen eingereicht.",
|
||||
"viewAsGroup": "Fortschritt ansehen von Gruppe...",
|
||||
"assignLearningPath": "Als Aufgabe geben",
|
||||
"group": "Gruppe",
|
||||
"description": "Beschreibung",
|
||||
"no-submission": "keine vorlage",
|
||||
"submission": "Einreichung",
|
||||
"progress": "Fortschritte",
|
||||
"remove": "entfernen",
|
||||
"students": "Studenten",
|
||||
"classJoinRequests": "Beitrittsanfragen",
|
||||
|
@ -100,5 +118,7 @@
|
|||
"accepted": "akzeptiert",
|
||||
"enterUsername": "Geben Sie den Benutzernamen der Lehrkraft ein, die Sie einladen möchten",
|
||||
"username": "Nutzername",
|
||||
"invite": "einladen"
|
||||
"invite": "einladen",
|
||||
"searchAllLearningPathsTitle": "Alle Lernpfade durchsuchen",
|
||||
"searchAllLearningPathsDescription": "Nicht gefunden, was Sie gesucht haben? Klicken Sie hier, um unsere gesamte Lernpfad-Datenbank zu durchsuchen."
|
||||
}
|
||||
|
|
|
@ -2,8 +2,8 @@
|
|||
"welcome": "Welcome",
|
||||
"student": "student",
|
||||
"teacher": "teacher",
|
||||
"assignments": "assignments",
|
||||
"classes": "classes",
|
||||
"assignments": "Assignments",
|
||||
"classes": "Classes",
|
||||
"discussions": "discussions",
|
||||
"logout": "log out",
|
||||
"error_title": "Error",
|
||||
|
@ -33,7 +33,7 @@
|
|||
"JoinClassExplanation": "Enter the code the teacher has given you to join the class.",
|
||||
"invalidFormat": "Invalid format.",
|
||||
"submitCode": "submit",
|
||||
"members": "members",
|
||||
"members": "Members",
|
||||
"themes": "Themes",
|
||||
"choose-theme": "Select a theme",
|
||||
"choose-age": "Select age",
|
||||
|
@ -57,8 +57,21 @@
|
|||
"older": "18 and older"
|
||||
},
|
||||
"read-more": "Read more",
|
||||
"code": "code",
|
||||
"new-assignment": "New Assignment",
|
||||
"edit-assignment": "Edit Assignment",
|
||||
"groups": "Groups",
|
||||
"learning-path": "Learning path",
|
||||
"choose-lp": "Select a learning path",
|
||||
"choose-classes": "Select classes",
|
||||
"create-groups": "Create groups",
|
||||
"title": "Title",
|
||||
"pick-class": "Pick a class",
|
||||
"choose-students": "Select students",
|
||||
"create-group": "Create group",
|
||||
"class": "class",
|
||||
"delete": "delete",
|
||||
"view-assignment": "View assignment",
|
||||
"code": "code",
|
||||
"invitations": "invitations",
|
||||
"createClass": "create class",
|
||||
"classname": "classname",
|
||||
|
@ -75,7 +88,6 @@
|
|||
"sent": "sent",
|
||||
"failed": "failed",
|
||||
"wrong": "something went wrong",
|
||||
"created": "created",
|
||||
"callbackLoading": "You are being logged in...",
|
||||
"loginUnexpectedError": "Login failed",
|
||||
"submitSolution": "Submit solution",
|
||||
|
@ -89,6 +101,12 @@
|
|||
"noSubmissionsYet": "No submissions yet.",
|
||||
"viewAsGroup": "View progress of group...",
|
||||
"assignLearningPath": "assign",
|
||||
"group": "Group",
|
||||
"description": "Description",
|
||||
"no-submission": "no submission",
|
||||
"submission": "Submission",
|
||||
"progress": "Progress",
|
||||
"created": "created",
|
||||
"remove": "remove",
|
||||
"students": "students",
|
||||
"classJoinRequests": "join requests",
|
||||
|
@ -100,5 +118,7 @@
|
|||
"rejected": "rejected",
|
||||
"enterUsername": "enter the username of the teacher you would like to invite",
|
||||
"username": "username",
|
||||
"invite": "invite"
|
||||
"invite": "invite",
|
||||
"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."
|
||||
}
|
||||
|
|
|
@ -33,7 +33,7 @@
|
|||
"JoinClassExplanation": "Entrez le code que l'enseignant vous a donné pour rejoindre la classe.",
|
||||
"invalidFormat": "Format non valide.",
|
||||
"submitCode": "envoyer",
|
||||
"members": "membres",
|
||||
"members": "Membres",
|
||||
"themes": "Thèmes",
|
||||
"choose-theme": "Choisis un thème",
|
||||
"choose-age": "Choisis un âge",
|
||||
|
@ -57,8 +57,21 @@
|
|||
"older": "18 et plus"
|
||||
},
|
||||
"read-more": "En savoir plus",
|
||||
"code": "code",
|
||||
"new-assignment": "Nouveau travail",
|
||||
"edit-assignment": "Modifier le travail",
|
||||
"groups": "Groupes",
|
||||
"learning-path": "Parcours d'apprentissage",
|
||||
"choose-lp": "Choisissez un parcours d'apprentissage",
|
||||
"choose-classes": "Choisissez des classes",
|
||||
"create-groups": "Créer des groupes",
|
||||
"title": "Titre",
|
||||
"pick-class": "Choisissez une classe",
|
||||
"choose-students": "Sélectionnez des élèves",
|
||||
"create-group": "Créer un groupe",
|
||||
"class": "classe",
|
||||
"delete": "supprimer",
|
||||
"view-assignment": "Voir le travail",
|
||||
"code": "code",
|
||||
"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.",
|
||||
|
@ -89,6 +102,11 @@
|
|||
"noSubmissionsYet": "Pas encore de soumissions.",
|
||||
"viewAsGroup": "Voir la progression du groupe...",
|
||||
"assignLearningPath": "donner comme tâche",
|
||||
"group": "Groupe",
|
||||
"description": "Description",
|
||||
"no-submission": "aucune soumission",
|
||||
"submission": "Soumission",
|
||||
"progress": "Progrès",
|
||||
"remove": "supprimer",
|
||||
"students": "étudiants",
|
||||
"classJoinRequests": "demandes d'adhésion",
|
||||
|
@ -100,5 +118,7 @@
|
|||
"rejected": "rejetée",
|
||||
"enterUsername": "entrez le nom d'utilisateur de l'enseignant que vous souhaitez inviter",
|
||||
"username": "Nom d'utilisateur",
|
||||
"invite": "inviter"
|
||||
"invite": "inviter",
|
||||
"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."
|
||||
}
|
||||
|
|
|
@ -2,8 +2,8 @@
|
|||
"welcome": "Welkom",
|
||||
"student": "leerling",
|
||||
"teacher": "leerkracht",
|
||||
"assignments": "opdrachten",
|
||||
"classes": "klassen",
|
||||
"assignments": "Opdrachten",
|
||||
"classes": "Klassen",
|
||||
"discussions": "discussies",
|
||||
"logout": "log uit",
|
||||
"error_title": "Fout",
|
||||
|
@ -33,7 +33,7 @@
|
|||
"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",
|
||||
"members": "Leden",
|
||||
"themes": "Lesthema's",
|
||||
"choose-theme": "Kies een thema",
|
||||
"choose-age": "Kies een leeftijd",
|
||||
|
@ -57,8 +57,21 @@
|
|||
"older": "Hoger onderwijs"
|
||||
},
|
||||
"read-more": "Lees meer",
|
||||
"code": "code",
|
||||
"new-assignment": "Nieuwe opdracht",
|
||||
"edit-assignment": "Opdracht bewerken",
|
||||
"groups": "Groepen",
|
||||
"learning-path": "Leerpad",
|
||||
"choose-lp": "Kies een leerpad",
|
||||
"choose-classes": "Kies klassen",
|
||||
"create-groups": "Groepen maken",
|
||||
"title": "Titel",
|
||||
"pick-class": "Kies een klas",
|
||||
"choose-students": "Studenten selecteren",
|
||||
"create-group": "Groep aanmaken",
|
||||
"class": "klas",
|
||||
"delete": "verwijderen",
|
||||
"view-assignment": "Opdracht bekijken",
|
||||
"code": "code",
|
||||
"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.",
|
||||
|
@ -89,6 +102,11 @@
|
|||
"noSubmissionsYet": "Nog geen indieningen.",
|
||||
"viewAsGroup": "Vooruitgang bekijken van groep...",
|
||||
"assignLearningPath": "Als opdracht geven",
|
||||
"group": "Groep",
|
||||
"description": "Beschrijving",
|
||||
"no-submission": "geen indiening",
|
||||
"submission": "Indiening",
|
||||
"progress": "Vooruitgang",
|
||||
"remove": "verwijder",
|
||||
"students": "studenten",
|
||||
"classJoinRequests": "deelname verzoeken",
|
||||
|
@ -100,5 +118,7 @@
|
|||
"rejected": "geweigerd",
|
||||
"enterUsername": "vul de gebruikersnaam van de leerkracht die je wilt uitnodigen in",
|
||||
"username": "gebruikersnaam",
|
||||
"invite": "uitnodigen"
|
||||
"invite": "uitnodigen",
|
||||
"searchAllLearningPathsTitle": "Alle leerpaden doorzoeken",
|
||||
"searchAllLearningPathsDescription": "Niet gevonden waar je naar op zoek was? Klik hier om onze volledige databank van beschikbare leerpaden te doorzoeken."
|
||||
}
|
||||
|
|
|
@ -12,11 +12,9 @@ import App from "./App.vue";
|
|||
import router from "./router";
|
||||
import { aliases, mdi } from "vuetify/iconsets/mdi";
|
||||
import { VueQueryPlugin, QueryClient } from "@tanstack/vue-query";
|
||||
import authService from "./services/auth/auth-service.ts";
|
||||
|
||||
const app = createApp(App);
|
||||
|
||||
await authService.loadUser();
|
||||
app.use(router);
|
||||
|
||||
const link = document.createElement("link");
|
||||
|
|
|
@ -104,7 +104,7 @@ export function useAssignmentsQuery(
|
|||
export function useAssignmentQuery(
|
||||
classid: MaybeRefOrGetter<string | undefined>,
|
||||
assignmentNumber: MaybeRefOrGetter<number | undefined>,
|
||||
): UseQueryReturnType<AssignmentsResponse, Error> {
|
||||
): UseQueryReturnType<AssignmentResponse, Error> {
|
||||
const { cid, an } = toValues(classid, assignmentNumber, 1, true);
|
||||
|
||||
return useQuery({
|
||||
|
@ -146,7 +146,7 @@ export function useDeleteAssignmentMutation(): UseMutationReturnType<
|
|||
|
||||
await invalidateAllAssignmentKeys(queryClient, cid, an);
|
||||
await invalidateAllGroupKeys(queryClient, cid, an);
|
||||
await invalidateAllSubmissionKeys(queryClient, cid, an);
|
||||
await invalidateAllSubmissionKeys(queryClient, undefined, undefined, undefined, cid, an);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
|
|
@ -46,3 +46,16 @@ export function useSearchLearningPathQuery(
|
|||
enabled: () => Boolean(toValue(query)),
|
||||
});
|
||||
}
|
||||
|
||||
export function useGetAllLearningPaths(
|
||||
language: MaybeRefOrGetter<string | undefined>,
|
||||
): UseQueryReturnType<LearningPath[], Error> {
|
||||
return useQuery({
|
||||
queryKey: [LEARNING_PATH_KEY, "getAllLearningPaths", language],
|
||||
queryFn: async () => {
|
||||
const lang = toValue(language);
|
||||
return learningPathController.getAllLearningPaths(lang);
|
||||
},
|
||||
enabled: () => Boolean(toValue(language)),
|
||||
});
|
||||
}
|
||||
|
|
|
@ -1,8 +1,10 @@
|
|||
import { computed, toValue } from "vue";
|
||||
import { computed, type Ref, toValue } from "vue";
|
||||
import type { MaybeRefOrGetter } from "vue";
|
||||
import {
|
||||
type QueryObserverResult,
|
||||
useMutation,
|
||||
type UseMutationReturnType,
|
||||
useQueries,
|
||||
useQuery,
|
||||
useQueryClient,
|
||||
type UseQueryReturnType,
|
||||
|
@ -70,6 +72,20 @@ export function useStudentQuery(
|
|||
});
|
||||
}
|
||||
|
||||
export function useStudentsByUsernamesQuery(
|
||||
usernames: MaybeRefOrGetter<string[] | undefined>,
|
||||
): Ref<QueryObserverResult<StudentResponse>[]> {
|
||||
const resolvedUsernames = toValue(usernames) ?? [];
|
||||
|
||||
return useQueries({
|
||||
queries: resolvedUsernames?.map((username) => ({
|
||||
queryKey: computed(() => studentQueryKey(toValue(username))),
|
||||
queryFn: async () => studentController.getByUsername(toValue(username)),
|
||||
enabled: Boolean(toValue(username)),
|
||||
})),
|
||||
});
|
||||
}
|
||||
|
||||
export function useStudentClassesQuery(
|
||||
username: MaybeRefOrGetter<string | undefined>,
|
||||
full: MaybeRefOrGetter<boolean> = true,
|
||||
|
|
|
@ -7,13 +7,13 @@ import CreateAssignment from "@/views/assignments/CreateAssignment.vue";
|
|||
import CreateDiscussion from "@/views/discussions/CreateDiscussion.vue";
|
||||
import CallbackPage from "@/views/CallbackPage.vue";
|
||||
import UserClasses from "@/views/classes/UserClasses.vue";
|
||||
import UserAssignments from "@/views/classes/UserAssignments.vue";
|
||||
import authService from "@/services/auth/auth-service.ts";
|
||||
import UserAssignments from "@/views/assignments/UserAssignments.vue";
|
||||
import LearningPathPage from "@/views/learning-paths/LearningPathPage.vue";
|
||||
import LearningPathSearchPage from "@/views/learning-paths/LearningPathSearchPage.vue";
|
||||
import UserHomePage from "@/views/homepage/UserHomePage.vue";
|
||||
import SingleTheme from "@/views/SingleTheme.vue";
|
||||
import LearningObjectView from "@/views/learning-paths/learning-object/LearningObjectView.vue";
|
||||
import authService from "@/services/auth/auth-service";
|
||||
|
||||
const router = createRouter({
|
||||
history: createWebHistory(import.meta.env.BASE_URL),
|
||||
|
@ -72,16 +72,20 @@ const router = createRouter({
|
|||
meta: { requiresAuth: true },
|
||||
},
|
||||
{
|
||||
path: "/assignment/create",
|
||||
name: "CreateAssigment",
|
||||
component: CreateAssignment,
|
||||
meta: { requiresAuth: true },
|
||||
},
|
||||
{
|
||||
path: "/assignment/:id",
|
||||
name: "SingleAssigment",
|
||||
component: SingleAssignment,
|
||||
path: "/assignment",
|
||||
meta: { requiresAuth: true },
|
||||
children: [
|
||||
{
|
||||
path: "create",
|
||||
name: "CreateAssigment",
|
||||
component: CreateAssignment,
|
||||
},
|
||||
{
|
||||
path: ":classId/:id",
|
||||
name: "SingleAssigment",
|
||||
component: SingleAssignment,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: "/class/:id",
|
||||
|
@ -138,7 +142,7 @@ const router = createRouter({
|
|||
router.beforeEach(async (to, _from, next) => {
|
||||
// Verify if user is logged in before accessing certain routes
|
||||
if (to.meta.requiresAuth) {
|
||||
if (!authService.isLoggedIn.value) {
|
||||
if (!authService.isLoggedIn.value && !(await authService.loadUser())) {
|
||||
next("/login");
|
||||
} else {
|
||||
next();
|
||||
|
|
|
@ -37,7 +37,20 @@ async function loadUser(): Promise<User | null> {
|
|||
if (!activeRole) {
|
||||
return null;
|
||||
}
|
||||
const user = await (await getUserManagers())[activeRole].getUser();
|
||||
|
||||
const userManager = (await getUserManagers())[activeRole];
|
||||
let user = await userManager.getUser(); // Load the user from the local storage.
|
||||
if (!user) {
|
||||
// If the user is not in the local storage, he could still be authenticated in Keycloak.
|
||||
try {
|
||||
user = await userManager.signinSilent();
|
||||
} catch (_: unknown) {
|
||||
// When the user was previously logged in and then logged out, signinSilent throws an error.
|
||||
// In that case, the user is not authenticated anymore, so we set him to null.
|
||||
user = null;
|
||||
}
|
||||
}
|
||||
|
||||
setUserAuthInfo(user);
|
||||
authState.activeRole = activeRole ?? null;
|
||||
return user;
|
||||
|
|
76
frontend/src/utils/assignment-rules.ts
Normal file
76
frontend/src/utils/assignment-rules.ts
Normal file
|
@ -0,0 +1,76 @@
|
|||
/**
|
||||
* Validation rule for the assignment title.
|
||||
*
|
||||
* Ensures that the title is not empty.
|
||||
*/
|
||||
export const assignmentTitleRules = [
|
||||
(value: string): string | boolean => {
|
||||
if (value?.length >= 1) {
|
||||
return true;
|
||||
} // Title must not be empty
|
||||
return "Title cannot be empty.";
|
||||
},
|
||||
];
|
||||
|
||||
/**
|
||||
* Validation rule for the learning path selection.
|
||||
*
|
||||
* Ensures that a valid learning path is selected.
|
||||
*/
|
||||
export const learningPathRules = [
|
||||
(value: { hruid: string; title: string }): string | boolean => {
|
||||
if (value && value.hruid) {
|
||||
return true; // Valid if hruid is present
|
||||
}
|
||||
return "You must select a learning path.";
|
||||
},
|
||||
];
|
||||
|
||||
/**
|
||||
* Validation rule for the classes selection.
|
||||
*
|
||||
* Ensures that at least one class is selected.
|
||||
*/
|
||||
export const classRules = [
|
||||
(value: string): string | boolean => {
|
||||
if (value) {
|
||||
return true;
|
||||
}
|
||||
return "You must select at least one class.";
|
||||
},
|
||||
];
|
||||
|
||||
/**
|
||||
* Validation rule for the deadline field.
|
||||
*
|
||||
* Ensures that a valid deadline is selected and is in the future.
|
||||
*/
|
||||
export const deadlineRules = [
|
||||
(value: string): string | boolean => {
|
||||
if (!value) {
|
||||
return "You must set a deadline.";
|
||||
}
|
||||
|
||||
const selectedDateTime = new Date(value);
|
||||
const now = new Date();
|
||||
|
||||
if (isNaN(selectedDateTime.getTime())) {
|
||||
return "Invalid date or time.";
|
||||
}
|
||||
|
||||
if (selectedDateTime <= now) {
|
||||
return "The deadline must be in the future.";
|
||||
}
|
||||
|
||||
return true;
|
||||
},
|
||||
];
|
||||
|
||||
export const descriptionRules = [
|
||||
(value: string): string | boolean => {
|
||||
if (!value || value.trim() === "") {
|
||||
return "Description cannot be empty.";
|
||||
}
|
||||
return true;
|
||||
},
|
||||
];
|
|
@ -1,14 +1,258 @@
|
|||
<script setup lang="ts">
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { computed, onMounted, ref, watch } from "vue";
|
||||
import GroupSelector from "@/components/assignments/GroupSelector.vue";
|
||||
import { assignmentTitleRules, classRules, descriptionRules, learningPathRules } from "@/utils/assignment-rules.ts";
|
||||
import DeadlineSelector from "@/components/assignments/DeadlineSelector.vue";
|
||||
import auth from "@/services/auth/auth-service.ts";
|
||||
import { useTeacherClassesQuery } from "@/queries/teachers.ts";
|
||||
import { useRouter } from "vue-router";
|
||||
import { useGetAllLearningPaths } from "@/queries/learning-paths.ts";
|
||||
import UsingQueryResult from "@/components/UsingQueryResult.vue";
|
||||
import type { LearningPath } from "@/data-objects/learning-paths/learning-path.ts";
|
||||
import type { ClassesResponse } from "@/controllers/classes.ts";
|
||||
import type { AssignmentDTO } from "@dwengo-1/common/interfaces/assignment";
|
||||
import { useCreateAssignmentMutation } from "@/queries/assignments.ts";
|
||||
import { useRoute } from "vue-router";
|
||||
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
const { t, locale } = useI18n();
|
||||
const role = ref(auth.authState.activeRole);
|
||||
const username = ref<string>("");
|
||||
|
||||
onMounted(async () => {
|
||||
// Redirect student
|
||||
if (role.value === "student") {
|
||||
await router.push("/user");
|
||||
}
|
||||
|
||||
// Get the user's username
|
||||
const user = await auth.loadUser();
|
||||
username.value = user?.profile?.preferred_username ?? "";
|
||||
});
|
||||
|
||||
const language = computed(() => locale.value);
|
||||
const form = ref();
|
||||
|
||||
//Fetch all learning paths
|
||||
const learningPathsQueryResults = useGetAllLearningPaths(language);
|
||||
|
||||
// Fetch and store all the teacher's classes
|
||||
const classesQueryResults = useTeacherClassesQuery(username, true);
|
||||
|
||||
const selectedClass = ref(undefined);
|
||||
|
||||
const assignmentTitle = ref("");
|
||||
const selectedLearningPath = ref(route.query.hruid || undefined);
|
||||
|
||||
// Disable combobox when learningPath prop is passed
|
||||
const lpIsSelected = route.query.hruid !== undefined;
|
||||
const deadline = ref(null);
|
||||
const description = ref("");
|
||||
const groups = ref<string[][]>([]);
|
||||
|
||||
// New group is added to the list
|
||||
function addGroupToList(students: string[]): void {
|
||||
if (students.length) {
|
||||
groups.value = [...groups.value, students];
|
||||
}
|
||||
}
|
||||
|
||||
watch(selectedClass, () => {
|
||||
groups.value = [];
|
||||
});
|
||||
|
||||
const { mutate, data, isSuccess } = useCreateAssignmentMutation();
|
||||
|
||||
watch([isSuccess, data], async ([success, newData]) => {
|
||||
if (success && newData?.assignment) {
|
||||
await router.push(`/assignment/${newData.assignment.within}/${newData.assignment.id}`);
|
||||
}
|
||||
});
|
||||
|
||||
async function submitFormHandler(): Promise<void> {
|
||||
const { valid } = await form.value.validate();
|
||||
if (!valid) return;
|
||||
|
||||
let lp = selectedLearningPath.value;
|
||||
if (!lpIsSelected) {
|
||||
lp = selectedLearningPath.value?.hruid;
|
||||
}
|
||||
|
||||
const assignmentDTO: AssignmentDTO = {
|
||||
id: 0,
|
||||
within: selectedClass.value?.id || "",
|
||||
title: assignmentTitle.value,
|
||||
description: description.value,
|
||||
learningPath: lp || "",
|
||||
language: language.value,
|
||||
groups: groups.value,
|
||||
};
|
||||
|
||||
mutate({ cid: assignmentDTO.within, data: assignmentDTO });
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<main>
|
||||
Hier zou de pagina staan om een assignment aan te maken voor de leerpad met hruid {{ route.query.hruid }} en
|
||||
language {{ route.query.language }}. (Overschrijf dit)
|
||||
</main>
|
||||
<div class="main-container">
|
||||
<h1 class="title">{{ t("new-assignment") }}</h1>
|
||||
<v-card class="form-card">
|
||||
<v-form
|
||||
ref="form"
|
||||
class="form-container"
|
||||
validate-on="submit lazy"
|
||||
@submit.prevent="submitFormHandler"
|
||||
>
|
||||
<v-container class="step-container">
|
||||
<v-card-text>
|
||||
<v-text-field
|
||||
v-model="assignmentTitle"
|
||||
:label="t('title')"
|
||||
:rules="assignmentTitleRules"
|
||||
density="compact"
|
||||
variant="outlined"
|
||||
clearable
|
||||
required
|
||||
></v-text-field>
|
||||
</v-card-text>
|
||||
|
||||
<using-query-result
|
||||
:query-result="learningPathsQueryResults"
|
||||
v-slot="{ data }: { data: LearningPath[] }"
|
||||
>
|
||||
<v-card-text>
|
||||
<v-combobox
|
||||
v-model="selectedLearningPath"
|
||||
:items="data"
|
||||
:label="t('choose-lp')"
|
||||
:rules="learningPathRules"
|
||||
variant="outlined"
|
||||
clearable
|
||||
hide-details
|
||||
density="compact"
|
||||
append-inner-icon="mdi-magnify"
|
||||
item-title="title"
|
||||
item-value="hruid"
|
||||
required
|
||||
:disabled="lpIsSelected"
|
||||
:filter="
|
||||
(item, query: string) => item.title.toLowerCase().includes(query.toLowerCase())
|
||||
"
|
||||
></v-combobox>
|
||||
</v-card-text>
|
||||
</using-query-result>
|
||||
|
||||
<using-query-result
|
||||
:query-result="classesQueryResults"
|
||||
v-slot="{ data }: { data: ClassesResponse }"
|
||||
>
|
||||
<v-card-text>
|
||||
<v-combobox
|
||||
v-model="selectedClass"
|
||||
:items="data?.classes ?? []"
|
||||
:label="t('pick-class')"
|
||||
:rules="classRules"
|
||||
variant="outlined"
|
||||
clearable
|
||||
hide-details
|
||||
density="compact"
|
||||
append-inner-icon="mdi-magnify"
|
||||
item-title="displayName"
|
||||
item-value="id"
|
||||
required
|
||||
></v-combobox>
|
||||
</v-card-text>
|
||||
</using-query-result>
|
||||
|
||||
<GroupSelector
|
||||
:classId="selectedClass?.id"
|
||||
:groups="groups"
|
||||
@groupCreated="addGroupToList"
|
||||
/>
|
||||
|
||||
<!-- Counter for created groups -->
|
||||
<v-card-text v-if="groups.length">
|
||||
<strong>Created Groups: {{ groups.length }}</strong>
|
||||
</v-card-text>
|
||||
<DeadlineSelector v-model:deadline="deadline" />
|
||||
<v-card-text>
|
||||
<v-textarea
|
||||
v-model="description"
|
||||
:label="t('description')"
|
||||
variant="outlined"
|
||||
density="compact"
|
||||
auto-grow
|
||||
rows="3"
|
||||
:rules="descriptionRules"
|
||||
></v-textarea>
|
||||
</v-card-text>
|
||||
<v-card-text>
|
||||
<v-btn
|
||||
class="mt-2"
|
||||
color="secondary"
|
||||
type="submit"
|
||||
block
|
||||
>{{ t("submit") }}
|
||||
</v-btn>
|
||||
<v-btn
|
||||
to="/user/assignment"
|
||||
color="grey"
|
||||
block
|
||||
>{{ t("cancel") }}
|
||||
</v-btn>
|
||||
</v-card-text>
|
||||
</v-container>
|
||||
</v-form>
|
||||
</v-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
||||
<style scoped>
|
||||
.main-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.form-card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 55%;
|
||||
/*padding: 1%;*/
|
||||
}
|
||||
|
||||
.form-container {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.step-container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
flex-direction: column;
|
||||
min-height: 200px;
|
||||
}
|
||||
|
||||
@media (max-width: 1000px) {
|
||||
.form-card {
|
||||
width: 70%;
|
||||
padding: 1%;
|
||||
}
|
||||
|
||||
.step-container {
|
||||
min-height: 300px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 650px) {
|
||||
.form-card {
|
||||
width: 95%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -1,7 +1,75 @@
|
|||
<script setup lang="ts"></script>
|
||||
<script setup lang="ts">
|
||||
import auth from "@/services/auth/auth-service.ts";
|
||||
import { computed, type Ref, ref, watchEffect } from "vue";
|
||||
import StudentAssignment from "@/views/assignments/StudentAssignment.vue";
|
||||
import TeacherAssignment from "@/views/assignments/TeacherAssignment.vue";
|
||||
import { useRoute } from "vue-router";
|
||||
import type { Language } from "@/data-objects/language.ts";
|
||||
import { useGetLearningPathQuery } from "@/queries/learning-paths.ts";
|
||||
import type { LearningPath } from "@/data-objects/learning-paths/learning-path.ts";
|
||||
import type { GroupDTO } from "@dwengo-1/common/interfaces/group";
|
||||
|
||||
const role = auth.authState.activeRole;
|
||||
const isTeacher = computed(() => role === "teacher");
|
||||
|
||||
const route = useRoute();
|
||||
const classId = ref<string>(route.params.classId as string);
|
||||
const assignmentId = ref(Number(route.params.id));
|
||||
|
||||
function useGroupsWithProgress(
|
||||
groups: Ref<GroupDTO[]>,
|
||||
hruid: Ref<string>,
|
||||
language: Ref<string>,
|
||||
): { groupProgressMap: Map<number, number> } {
|
||||
const groupProgressMap: Map<number, number> = new Map<number, number>();
|
||||
|
||||
watchEffect(() => {
|
||||
// Clear existing entries to avoid stale data
|
||||
groupProgressMap.clear();
|
||||
|
||||
const lang = ref(language.value as Language);
|
||||
|
||||
groups.value.forEach((group) => {
|
||||
const groupKey = group.groupNumber;
|
||||
const forGroup = ref({
|
||||
forGroup: groupKey,
|
||||
assignmentNo: assignmentId,
|
||||
classId: classId,
|
||||
});
|
||||
|
||||
const query = useGetLearningPathQuery(hruid.value, lang, forGroup);
|
||||
|
||||
const data = query.data.value;
|
||||
|
||||
groupProgressMap.set(groupKey, data ? calculateProgress(data) : 0);
|
||||
});
|
||||
});
|
||||
|
||||
return {
|
||||
groupProgressMap,
|
||||
};
|
||||
}
|
||||
|
||||
function calculateProgress(lp: LearningPath): number {
|
||||
return ((lp.amountOfNodes - lp.amountOfNodesLeft) / lp.amountOfNodes) * 100;
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<main></main>
|
||||
<TeacherAssignment
|
||||
:class-id="classId"
|
||||
:assignment-id="assignmentId"
|
||||
:use-groups-with-progress="useGroupsWithProgress"
|
||||
v-if="isTeacher"
|
||||
>
|
||||
</TeacherAssignment>
|
||||
<StudentAssignment
|
||||
:class-id="classId"
|
||||
:assignment-id="assignmentId"
|
||||
:use-groups-with-progress="useGroupsWithProgress"
|
||||
v-else
|
||||
>
|
||||
</StudentAssignment>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
||||
|
|
167
frontend/src/views/assignments/StudentAssignment.vue
Normal file
167
frontend/src/views/assignments/StudentAssignment.vue
Normal file
|
@ -0,0 +1,167 @@
|
|||
<script setup lang="ts">
|
||||
import { ref, computed, type Ref } from "vue";
|
||||
import auth from "@/services/auth/auth-service.ts";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { useAssignmentQuery } from "@/queries/assignments.ts";
|
||||
import UsingQueryResult from "@/components/UsingQueryResult.vue";
|
||||
import type { AssignmentResponse } from "@/controllers/assignments.ts";
|
||||
import { asyncComputed } from "@vueuse/core";
|
||||
import { useStudentsByUsernamesQuery } from "@/queries/students.ts";
|
||||
import { useGroupsQuery } from "@/queries/groups.ts";
|
||||
import { useGetLearningPathQuery } from "@/queries/learning-paths.ts";
|
||||
import type { Language } from "@/data-objects/language.ts";
|
||||
import type { GroupDTO } from "@dwengo-1/common/interfaces/group";
|
||||
|
||||
const props = defineProps<{
|
||||
classId: string;
|
||||
assignmentId: number;
|
||||
useGroupsWithProgress: (
|
||||
groups: Ref<GroupDTO[]>,
|
||||
hruid: Ref<string>,
|
||||
language: Ref<Language>,
|
||||
) => { groupProgressMap: Map<number, number> };
|
||||
}>();
|
||||
|
||||
const { t, locale } = useI18n();
|
||||
const language = ref<Language>(locale.value as Language);
|
||||
const learningPath = ref();
|
||||
// Get the user's username/id
|
||||
const username = asyncComputed(async () => {
|
||||
const user = await auth.loadUser();
|
||||
return user?.profile?.preferred_username ?? undefined;
|
||||
});
|
||||
|
||||
const assignmentQueryResult = useAssignmentQuery(() => props.classId, props.assignmentId);
|
||||
learningPath.value = assignmentQueryResult.data.value?.assignment?.learningPath;
|
||||
|
||||
const submitted = ref(false); //TODO: update by fetching submissions and check if group submitted
|
||||
|
||||
const lpQueryResult = useGetLearningPathQuery(
|
||||
computed(() => assignmentQueryResult.data.value?.assignment?.learningPath ?? ""),
|
||||
computed(() => language.value),
|
||||
);
|
||||
|
||||
const groupsQueryResult = useGroupsQuery(props.classId, props.assignmentId, true);
|
||||
const group = computed(() =>
|
||||
groupsQueryResult?.data.value?.groups.find((group) =>
|
||||
group.members?.some((m) => m.username === username.value),
|
||||
),
|
||||
);
|
||||
|
||||
const _groupArray = computed(() => (group.value ? [group.value] : []));
|
||||
const progressValue = ref(0);
|
||||
/* Crashes right now cause api data has inexistent hruid TODO: uncomment later and use it in progress bar
|
||||
Const {groupProgressMap} = props.useGroupsWithProgress(
|
||||
groupArray,
|
||||
learningPath,
|
||||
language
|
||||
);
|
||||
*/
|
||||
|
||||
// Assuming group.value.members is a list of usernames TODO: case when it's StudentDTO's
|
||||
const studentQueries = useStudentsByUsernamesQuery(() => group.value?.members as string[]);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="container">
|
||||
<using-query-result
|
||||
:query-result="assignmentQueryResult"
|
||||
v-slot="{ data }: { data: AssignmentResponse }"
|
||||
>
|
||||
<v-card
|
||||
v-if="data"
|
||||
class="assignment-card"
|
||||
>
|
||||
<div class="top-buttons">
|
||||
<v-btn
|
||||
icon
|
||||
variant="text"
|
||||
class="back-btn"
|
||||
to="/user/assignment"
|
||||
>
|
||||
<v-icon>mdi-arrow-left</v-icon>
|
||||
</v-btn>
|
||||
|
||||
<v-chip
|
||||
v-if="submitted"
|
||||
class="ma-2 top-right-btn"
|
||||
label
|
||||
color="success"
|
||||
>
|
||||
{{ t("submitted") }}
|
||||
</v-chip>
|
||||
</div>
|
||||
<v-card-title class="text-h4 assignmentTopTitle">{{ data.assignment.title }}</v-card-title>
|
||||
|
||||
<v-card-subtitle class="subtitle-section">
|
||||
<using-query-result
|
||||
:query-result="lpQueryResult"
|
||||
v-slot="{ data: lpData }"
|
||||
>
|
||||
<v-btn
|
||||
v-if="lpData"
|
||||
:to="`/learningPath/${lpData.hruid}/${language}/${lpData.startNode.learningobjectHruid}?forGroup=${group?.groupNumber}&assignmentNo=${assignmentId}&classId=${classId}`"
|
||||
variant="tonal"
|
||||
color="primary"
|
||||
>
|
||||
{{ t("learning-path") }}
|
||||
</v-btn>
|
||||
</using-query-result>
|
||||
</v-card-subtitle>
|
||||
|
||||
<v-card-text class="description">
|
||||
{{ data.assignment.description }}
|
||||
</v-card-text>
|
||||
<v-card-text>
|
||||
<v-row
|
||||
align="center"
|
||||
no-gutters
|
||||
>
|
||||
<v-col cols="auto">
|
||||
<span class="progress-label">{{ t("progress") + ": " }}</span>
|
||||
</v-col>
|
||||
<v-col>
|
||||
<v-progress-linear
|
||||
:model-value="progressValue"
|
||||
color="primary"
|
||||
height="20"
|
||||
class="progress-bar"
|
||||
>
|
||||
<template v-slot:default="{ value }">
|
||||
<strong>{{ Math.ceil(value) }}%</strong>
|
||||
</template>
|
||||
</v-progress-linear>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-card-text>
|
||||
|
||||
<v-card-text class="group-section">
|
||||
<h3>{{ t("group") }}</h3>
|
||||
<div v-if="studentQueries">
|
||||
<ul>
|
||||
<li
|
||||
v-for="student in group?.members"
|
||||
:key="student.username"
|
||||
>
|
||||
{{ student.firstName + " " + student.lastName }}
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</using-query-result>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
@import "@/assets/assignment.css";
|
||||
|
||||
.progress-label {
|
||||
font-weight: bold;
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
.progress-bar {
|
||||
width: 40%;
|
||||
}
|
||||
</style>
|
|
@ -1,7 +0,0 @@
|
|||
<script setup lang="ts"></script>
|
||||
|
||||
<template>
|
||||
<main></main>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
234
frontend/src/views/assignments/TeacherAssignment.vue
Normal file
234
frontend/src/views/assignments/TeacherAssignment.vue
Normal file
|
@ -0,0 +1,234 @@
|
|||
<script setup lang="ts">
|
||||
import { computed, type Ref, ref } from "vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { useAssignmentQuery, useDeleteAssignmentMutation } from "@/queries/assignments.ts";
|
||||
import UsingQueryResult from "@/components/UsingQueryResult.vue";
|
||||
import { useGroupsQuery } from "@/queries/groups.ts";
|
||||
import { useGetLearningPathQuery } from "@/queries/learning-paths.ts";
|
||||
import type { Language } from "@/data-objects/language.ts";
|
||||
import type { AssignmentResponse } from "@/controllers/assignments.ts";
|
||||
import type { GroupDTO } from "@dwengo-1/common/interfaces/group";
|
||||
|
||||
const props = defineProps<{
|
||||
classId: string;
|
||||
assignmentId: number;
|
||||
useGroupsWithProgress: (
|
||||
groups: Ref<GroupDTO[]>,
|
||||
hruid: Ref<string>,
|
||||
language: Ref<Language>,
|
||||
) => { groupProgressMap: Map<number, number> };
|
||||
}>();
|
||||
|
||||
const { t, locale } = useI18n();
|
||||
const language = computed(() => locale.value);
|
||||
const groups = ref();
|
||||
const learningPath = ref();
|
||||
|
||||
const assignmentQueryResult = useAssignmentQuery(() => props.classId, props.assignmentId);
|
||||
learningPath.value = assignmentQueryResult.data.value?.assignment?.learningPath;
|
||||
// Get learning path object
|
||||
const lpQueryResult = useGetLearningPathQuery(
|
||||
computed(() => assignmentQueryResult.data.value?.assignment?.learningPath ?? ""),
|
||||
computed(() => language.value as Language),
|
||||
);
|
||||
|
||||
// Get all the groups withing the assignment
|
||||
const groupsQueryResult = useGroupsQuery(props.classId, props.assignmentId, true);
|
||||
groups.value = groupsQueryResult.data.value?.groups;
|
||||
|
||||
/* Crashes right now cause api data has inexistent hruid TODO: uncomment later and use it in progress bar
|
||||
Const {groupProgressMap} = props.useGroupsWithProgress(
|
||||
groups,
|
||||
learningPath,
|
||||
language
|
||||
);
|
||||
*/
|
||||
|
||||
const allGroups = computed(() => {
|
||||
const groups = groupsQueryResult.data.value?.groups;
|
||||
if (!groups) return [];
|
||||
|
||||
return groups.map((group) => ({
|
||||
name: `${t("group")} ${group.groupNumber}`,
|
||||
progress: 0, //GroupProgressMap[group.groupNumber],
|
||||
members: group.members,
|
||||
submitted: false, //TODO: fetch from submission
|
||||
}));
|
||||
});
|
||||
|
||||
const dialog = ref(false);
|
||||
const selectedGroup = ref({});
|
||||
|
||||
function openGroupDetails(group): void {
|
||||
selectedGroup.value = group;
|
||||
dialog.value = true;
|
||||
}
|
||||
|
||||
const headers = computed(() => [
|
||||
{ title: t("group"), align: "start", key: "name" },
|
||||
{ title: t("progress"), align: "center", key: "progress" },
|
||||
{ title: t("submission"), align: "center", key: "submission" },
|
||||
]);
|
||||
|
||||
const { mutate } = useDeleteAssignmentMutation();
|
||||
|
||||
async function deleteAssignment(num: number, clsId: string): Promise<void> {
|
||||
mutate(
|
||||
{ cid: clsId, an: num },
|
||||
{
|
||||
onSuccess: () => {
|
||||
window.location.href = "/user/assignment";
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="container">
|
||||
<using-query-result
|
||||
:query-result="assignmentQueryResult"
|
||||
v-slot="{ data }: { data: AssignmentResponse }"
|
||||
>
|
||||
<v-card
|
||||
v-if="data"
|
||||
class="assignment-card"
|
||||
>
|
||||
<div class="top-buttons">
|
||||
<v-btn
|
||||
icon
|
||||
variant="text"
|
||||
class="back-btn"
|
||||
to="/user/assignment"
|
||||
>
|
||||
<v-icon>mdi-arrow-left</v-icon>
|
||||
</v-btn>
|
||||
|
||||
<v-btn
|
||||
icon
|
||||
variant="text"
|
||||
class="top-right-btn"
|
||||
@click="deleteAssignment(data.assignment.id, data.assignment.within)"
|
||||
>
|
||||
<v-icon>mdi-delete</v-icon>
|
||||
</v-btn>
|
||||
</div>
|
||||
<v-card-title class="text-h4 assignmentTopTitle">{{ data.assignment.title }}</v-card-title>
|
||||
<v-card-subtitle class="subtitle-section">
|
||||
<using-query-result
|
||||
:query-result="lpQueryResult"
|
||||
v-slot="{ data: lpData }"
|
||||
>
|
||||
<v-btn
|
||||
v-if="lpData"
|
||||
:to="`/learningPath/${lpData.hruid}/${language}/${lpData.startNode.learningobjectHruid}?assignmentNo=${assignmentId}&classId=${classId}`"
|
||||
variant="tonal"
|
||||
color="primary"
|
||||
>
|
||||
{{ t("learning-path") }}
|
||||
</v-btn>
|
||||
</using-query-result>
|
||||
</v-card-subtitle>
|
||||
|
||||
<v-card-text class="description">
|
||||
{{ data.assignment.description }}
|
||||
</v-card-text>
|
||||
|
||||
<v-card-text class="group-section">
|
||||
<h3>{{ t("groups") }}</h3>
|
||||
<div class="table-scroll">
|
||||
<v-data-table
|
||||
:headers="headers"
|
||||
:items="allGroups"
|
||||
item-key="id"
|
||||
class="elevation-1"
|
||||
>
|
||||
<template #[`item.name`]="{ item }">
|
||||
<v-btn
|
||||
@click="openGroupDetails(item)"
|
||||
variant="text"
|
||||
color="primary"
|
||||
>
|
||||
{{ item.name }}
|
||||
</v-btn>
|
||||
</template>
|
||||
|
||||
<template #[`item.progress`]="{ item }">
|
||||
<v-progress-linear
|
||||
:model-value="item.progress"
|
||||
color="blue-grey"
|
||||
height="25"
|
||||
>
|
||||
<template v-slot:default="{ value }">
|
||||
<strong>{{ Math.ceil(value) }}%</strong>
|
||||
</template>
|
||||
</v-progress-linear>
|
||||
</template>
|
||||
|
||||
<template #[`item.submission`]="{ item }">
|
||||
<v-btn
|
||||
:to="item.submitted ? `${props.assignmentId}/submissions/` : undefined"
|
||||
:color="item.submitted ? 'green' : 'red'"
|
||||
variant="text"
|
||||
class="text-capitalize"
|
||||
>
|
||||
{{ item.submitted ? t("see-submission") : t("no-submission") }}
|
||||
</v-btn>
|
||||
</template>
|
||||
</v-data-table>
|
||||
</div>
|
||||
</v-card-text>
|
||||
|
||||
<v-dialog
|
||||
v-model="dialog"
|
||||
max-width="50%"
|
||||
>
|
||||
<v-card>
|
||||
<v-card-title class="headline">{{ t("members") }}</v-card-title>
|
||||
<v-card-text>
|
||||
<v-list>
|
||||
<v-list-item
|
||||
v-for="(member, index) in selectedGroup.members"
|
||||
:key="index"
|
||||
>
|
||||
<v-list-item-content>
|
||||
<v-list-item-title
|
||||
>{{ member.firstName + " " + member.lastName }}
|
||||
</v-list-item-title>
|
||||
</v-list-item-content>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
</v-card-text>
|
||||
<v-card-actions>
|
||||
<v-btn
|
||||
color="primary"
|
||||
@click="dialog = false"
|
||||
>Close</v-btn
|
||||
>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
<!--
|
||||
<v-card-actions class="justify-end">
|
||||
<v-btn
|
||||
size="large"
|
||||
color="success"
|
||||
variant="text"
|
||||
>
|
||||
{{ t("view-submissions") }}
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
-->
|
||||
</v-card>
|
||||
</using-query-result>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
@import "@/assets/assignment.css";
|
||||
|
||||
.table-scroll {
|
||||
overflow-x: auto;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
}
|
||||
</style>
|
|
@ -1,7 +0,0 @@
|
|||
<script setup lang="ts"></script>
|
||||
|
||||
<template>
|
||||
<main></main>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
188
frontend/src/views/assignments/UserAssignments.vue
Normal file
188
frontend/src/views/assignments/UserAssignments.vue
Normal file
|
@ -0,0 +1,188 @@
|
|||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted, watch } from "vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { useRouter } from "vue-router";
|
||||
import auth from "@/services/auth/auth-service.ts";
|
||||
import { useTeacherClassesQuery } from "@/queries/teachers.ts";
|
||||
import { useStudentClassesQuery } from "@/queries/students.ts";
|
||||
import { ClassController } from "@/controllers/classes.ts";
|
||||
import type { ClassDTO } from "@dwengo-1/common/interfaces/class";
|
||||
import { asyncComputed } from "@vueuse/core";
|
||||
import { useDeleteAssignmentMutation } from "@/queries/assignments.ts";
|
||||
|
||||
const { t } = useI18n();
|
||||
const router = useRouter();
|
||||
|
||||
const role = ref(auth.authState.activeRole);
|
||||
const username = ref<string>("");
|
||||
|
||||
const isTeacher = computed(() => role.value === "teacher");
|
||||
|
||||
// Fetch and store all the teacher's classes
|
||||
let classesQueryResults = undefined;
|
||||
|
||||
if (isTeacher.value) {
|
||||
classesQueryResults = useTeacherClassesQuery(username, true);
|
||||
} else {
|
||||
classesQueryResults = useStudentClassesQuery(username, true);
|
||||
}
|
||||
|
||||
//TODO: remove later
|
||||
const classController = new ClassController();
|
||||
|
||||
//TODO: replace by query that fetches all user's assignment
|
||||
const assignments = asyncComputed(async () => {
|
||||
const classes = classesQueryResults?.data?.value?.classes;
|
||||
if (!classes) return [];
|
||||
const result = await Promise.all(
|
||||
(classes as ClassDTO[]).map(async (cls) => {
|
||||
const { assignments } = await classController.getAssignments(cls.id);
|
||||
return assignments.map((a) => ({
|
||||
id: a.id,
|
||||
class: cls,
|
||||
title: a.title,
|
||||
description: a.description,
|
||||
learningPath: a.learningPath,
|
||||
language: a.language,
|
||||
groups: a.groups,
|
||||
}));
|
||||
}),
|
||||
);
|
||||
|
||||
return result.flat();
|
||||
}, []);
|
||||
|
||||
async function goToCreateAssignment(): Promise<void> {
|
||||
await router.push("/assignment/create");
|
||||
}
|
||||
|
||||
async function goToAssignmentDetails(id: number, clsId: string): Promise<void> {
|
||||
await router.push(`/assignment/${clsId}/${id}`);
|
||||
}
|
||||
|
||||
const { mutate, data, isSuccess } = useDeleteAssignmentMutation();
|
||||
|
||||
watch([isSuccess, data], async ([success, oldData]) => {
|
||||
if (success && oldData?.assignment) {
|
||||
window.location.reload();
|
||||
}
|
||||
});
|
||||
|
||||
async function goToDeleteAssignment(num: number, clsId: string): Promise<void> {
|
||||
mutate({ cid: clsId, an: num });
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
const user = await auth.loadUser();
|
||||
username.value = user?.profile?.preferred_username ?? "";
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="assignments-container">
|
||||
<h1>{{ t("assignments") }}</h1>
|
||||
|
||||
<v-btn
|
||||
v-if="isTeacher"
|
||||
color="primary"
|
||||
class="mb-4 center-btn"
|
||||
@click="goToCreateAssignment"
|
||||
>
|
||||
{{ t("new-assignment") }}
|
||||
</v-btn>
|
||||
|
||||
<v-container>
|
||||
<v-row>
|
||||
<v-col
|
||||
v-for="assignment in assignments"
|
||||
:key="assignment.id"
|
||||
cols="12"
|
||||
>
|
||||
<v-card class="assignment-card">
|
||||
<div class="top-content">
|
||||
<div class="assignment-title">{{ assignment.title }}</div>
|
||||
<div class="assignment-class">
|
||||
{{ t("class") }}:
|
||||
<span class="class-name">
|
||||
{{ assignment.class.displayName }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="spacer"></div>
|
||||
|
||||
<div class="button-row">
|
||||
<v-btn
|
||||
color="primary"
|
||||
variant="text"
|
||||
@click="goToAssignmentDetails(assignment.id, assignment.class.id)"
|
||||
>
|
||||
{{ t("view-assignment") }}
|
||||
</v-btn>
|
||||
<v-btn
|
||||
v-if="isTeacher"
|
||||
color="red"
|
||||
variant="text"
|
||||
@click="goToDeleteAssignment(assignment.id, assignment.class.id)"
|
||||
>
|
||||
{{ t("delete") }}
|
||||
</v-btn>
|
||||
</div>
|
||||
</v-card>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-container>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.assignments-container {
|
||||
width: 100%;
|
||||
margin: 0 auto;
|
||||
padding: 2% 4%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.center-btn {
|
||||
display: block;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
.assignment-card {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.top-content {
|
||||
margin-bottom: 1rem;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.spacer {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.button-row {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 0.5rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.assignment-title {
|
||||
font-weight: bold;
|
||||
font-size: 1.5rem;
|
||||
margin-bottom: 0.1rem;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.assignment-class {
|
||||
color: #666;
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
|
||||
.class-name {
|
||||
font-weight: 500;
|
||||
color: #333;
|
||||
}
|
||||
</style>
|
|
@ -1,7 +0,0 @@
|
|||
<script setup lang="ts"></script>
|
||||
|
||||
<template>
|
||||
<main></main>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
10222
package-lock.json
generated
10222
package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
@ -41,5 +41,8 @@
|
|||
"eslint-config-prettier": "^10.0.1",
|
||||
"jiti": "^2.4.2",
|
||||
"typescript-eslint": "^8.24.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"swagger": "^0.7.5"
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue