style: fix linting issues met Prettier

This commit is contained in:
Lint Action 2025-05-17 18:47:26 +00:00
parent 5b00066106
commit 323d66bbcb
12 changed files with 908 additions and 898 deletions

View file

@ -39,9 +39,9 @@ export async function getAllAssignmentsHandler(req: Request, res: Response): Pro
export async function createAssignmentHandler(req: Request, res: Response): Promise<void> {
const classid = req.params.classid;
const description = req.body.description || "";
const description = req.body.description || '';
const language = req.body.language || FALLBACK_LANG;
const learningPath = req.body.learningPath || "";
const learningPath = req.body.learningPath || '';
const title = req.body.title;
requireFields({ title });

View file

@ -20,7 +20,7 @@ export class AssignmentRepository extends DwengoEntityRepository<Assignment> {
},
},
},
populate: ['groups', 'groups.members']
populate: ['groups', 'groups.members'],
});
}
public async findAllAssignmentsInClass(within: Class): Promise<Assignment[]> {

View file

@ -31,7 +31,6 @@ router.get('/:username/students', preventImpersonation, getTeacherStudentHandler
router.get(`/:username/assignments`, getTeacherAssignmentsHandler);
router.get('/:username/joinRequests/:classId', onlyAllowTeacherOfClass, getStudentJoinRequestHandler);
router.put('/:username/joinRequests/:classId/:studentUsername', onlyAllowTeacherOfClass, updateStudentJoinRequestHandler);

View file

@ -103,20 +103,22 @@ export async function putAssignment(classid: string, id: number, assignmentData:
if (assignmentData.groups) {
const hasDuplicates = (arr: string[]) => new Set(arr).size !== arr.length;
if (hasDuplicates(assignmentData.groups.flat() as string[])) {
throw new BadRequestException("Student can only be in one group");
throw new BadRequestException('Student can only be in one group');
}
const studentLists = await Promise.all((assignmentData.groups as string[][]).map(async group => await fetchStudents(group)));
const studentLists = await Promise.all((assignmentData.groups as string[][]).map(async (group) => await fetchStudents(group)));
const groupRepository = getGroupRepository();
await groupRepository.deleteAllByAssignment(assignment);
await Promise.all(studentLists.map(async students => {
const newGroup = groupRepository.create({
assignment: assignment,
members: students,
});
await groupRepository.save(newGroup);
}));
await Promise.all(
studentLists.map(async (students) => {
const newGroup = groupRepository.create({
assignment: assignment,
members: students,
});
await groupRepository.save(newGroup);
})
);
delete assignmentData.groups;
}

View file

@ -1,9 +1,4 @@
import {
getAssignmentRepository,
getClassJoinRequestRepository,
getClassRepository,
getTeacherRepository,
} from '../data/repositories.js';
import { getAssignmentRepository, getClassJoinRequestRepository, getClassRepository, getTeacherRepository } from '../data/repositories.js';
import { mapToClassDTO } from '../interfaces/class.js';
import { mapToTeacher, mapToTeacherDTO } from '../interfaces/teacher.js';
import { Teacher } from '../entities/users/teacher.entity.js';

View file

@ -3,7 +3,7 @@
import UsingQueryResult from "@/components/UsingQueryResult.vue";
import { useAssignmentSubmissionsQuery } from "@/queries/assignments.ts";
import type { SubmissionsResponse } from "@/controllers/submissions.ts";
import {watch} from "vue";
import { watch } from "vue";
const props = defineProps<{
group: object;
@ -29,7 +29,7 @@
emit("update:hasSubmission", data.submissions.length > 0);
}
},
{ immediate: true }
{ immediate: true },
);
</script>

View file

@ -6,8 +6,7 @@
const datetime = ref("");
datetime.value = props.deadline ? new Date(props.deadline).toISOString().slice(0, 16) : ""
datetime.value = props.deadline ? new Date(props.deadline).toISOString().slice(0, 16) : "";
// Watch the datetime value and emit the update
watch(datetime, (val) => {
@ -21,7 +20,6 @@
const deadlineRules = [
(value: string): string | boolean => {
const selectedDateTime = new Date(value);
const now = new Date();

View file

@ -1,281 +1,135 @@
<script setup lang="ts">
import {computed, ref, watch} from "vue";
import {useI18n} from "vue-i18n";
import {useClassStudentsQuery} from "@/queries/classes";
import { computed, ref, watch } from "vue";
import { useI18n } from "vue-i18n";
import { useClassStudentsQuery } from "@/queries/classes";
const props = defineProps<{
classId: string | undefined;
groups: object[];
}>();
const emit = defineEmits(["close", "groupsUpdated", "done"]);
const {t} = useI18n();
const props = defineProps<{
classId: string | undefined;
groups: object[];
}>();
const emit = defineEmits(["close", "groupsUpdated", "done"]);
const { t } = useI18n();
interface StudentItem {
username: string;
fullName: string;
}
const {data: studentsData} = useClassStudentsQuery(() => props.classId, true);
// Dialog states for group editing
const activeDialog = ref<"random" | "dragdrop" | null>(null);
// Drag state for the drag and drop
const draggedItem = ref<{ groupIndex: number, studentIndex: number } | null>(null);
const currentGroups = ref<StudentItem[][]>([]);
const unassignedStudents = ref<StudentItem[]>([]);
const allStudents = ref<StudentItem[]>([]);
// Random groups state
const groupSize = ref(1);
const randomGroupsPreview = ref<StudentItem[][]>([]);
// Initialize data
watch(
() => [studentsData.value, props.groups],
([studentsVal, existingGroups]) => {
if (!studentsVal) return;
// Initialize all students
allStudents.value = studentsVal.students.map((s) => ({
username: s.username,
fullName: `${s.firstName} ${s.lastName}`,
}));
// Initialize groups if they exist
if (existingGroups && existingGroups.length > 0) {
currentGroups.value = existingGroups.map((group) =>
group.members.map(member => ({
username: member.username,
fullName: `${member.firstName} ${member.lastName}`
}))
);
const assignedUsernames = new Set(
existingGroups.flatMap((g) => g.members.map((m: StudentItem) => m.username)),
);
unassignedStudents.value = allStudents.value.filter((s) => !assignedUsernames.has(s.username));
} else {
currentGroups.value = [];
unassignedStudents.value = [...allStudents.value];
}
randomGroupsPreview.value = [...currentGroups.value];
},
{immediate: true},
);
/** Random groups functions */
function generateRandomGroups(): void {
if (groupSize.value < 1) return;
// Shuffle students
const shuffled = [...allStudents.value].sort(() => Math.random() - 0.5);
// Create new groups
const newGroups: StudentItem[][] = [];
const groupCount = Math.ceil(shuffled.length / groupSize.value);
for (let i = 0; i < groupCount; i++) {
newGroups.push([]);
interface StudentItem {
username: string;
fullName: string;
}
// Distribute students
shuffled.forEach((student, index) => {
const groupIndex = index % groupCount;
newGroups[groupIndex].push(student);
});
const { data: studentsData } = useClassStudentsQuery(() => props.classId, true);
randomGroupsPreview.value = newGroups;
}
// Dialog states for group editing
const activeDialog = ref<"random" | "dragdrop" | null>(null);
function saveRandomGroups(): void {
if (randomGroupsPreview.value.length === 0) {
alert(t("please-generate-groups-first"));
return;
}
// Drag state for the drag and drop
const draggedItem = ref<{ groupIndex: number; studentIndex: number } | null>(null);
emit(
"groupsUpdated",
randomGroupsPreview.value.map((g) => g.map((s) => s.username)),
);
activeDialog.value = null;
emit("done");
emit("close");
}
const currentGroups = ref<StudentItem[][]>([]);
const unassignedStudents = ref<StudentItem[]>([]);
const allStudents = ref<StudentItem[]>([]);
function addNewGroup() {
currentGroups.value.push([]);
}
// Random groups state
const groupSize = ref(1);
const randomGroupsPreview = ref<StudentItem[][]>([]);
function removeGroup(index: number) {
// Move students back to unassigned
unassignedStudents.value.push(...currentGroups.value[index]);
currentGroups.value.splice(index, 1);
}
// Initialize data
watch(
() => [studentsData.value, props.groups],
([studentsVal, existingGroups]) => {
if (!studentsVal) return;
/** Drag and drop functions */
// Initialize all students
allStudents.value = studentsVal.students.map((s) => ({
username: s.username,
fullName: `${s.firstName} ${s.lastName}`,
}));
// Touch state interface
interface TouchState {
isDragging: boolean;
startX: number;
startY: number;
currentGroupIndex: number;
currentStudentIndex: number;
element: HTMLElement | null;
clone: HTMLElement | null;
originalRect: DOMRect | null;
hasMoved: boolean;
}
const touchState = ref<TouchState>({
isDragging: false,
startX: 0,
startY: 0,
currentGroupIndex: -1,
currentStudentIndex: -1,
element: null,
clone: null,
originalRect: null,
hasMoved: false
});
function handleTouchStart(event: TouchEvent, groupIndex: number, studentIndex: number): void {
if (event.touches.length > 1) return;
const touch = event.touches[0];
const target = event.target as HTMLElement;
// Target the chip directly instead of the draggable container
const chip = target.closest('.v-chip') as HTMLElement;
if (!chip) return;
// Get the chip's position relative to the viewport
const rect = chip.getBoundingClientRect();
touchState.value = {
isDragging: true,
startX: touch.clientX,
startY: touch.clientY,
currentGroupIndex: groupIndex,
currentStudentIndex: studentIndex,
element: chip,
clone: null,
originalRect: rect,
hasMoved: false
};
// Clone only the chip
const clone = chip.cloneNode(true) as HTMLElement;
clone.classList.add('drag-clone');
clone.style.position = 'fixed';
clone.style.zIndex = '10000';
clone.style.opacity = '0.9';
clone.style.pointerEvents = 'none';
clone.style.width = `${rect.width}px`;
clone.style.height = `${rect.height}px`;
clone.style.left = `${rect.left}px`;
clone.style.top = `${rect.top}px`;
clone.style.transform = 'scale(1.05)';
clone.style.boxShadow = '0 4px 8px rgba(0,0,0,0.3)';
clone.style.transition = 'transform 0.1s';
// Ensure the clone has the same chip styling
clone.style.backgroundColor = getComputedStyle(chip).backgroundColor;
clone.style.color = getComputedStyle(chip).color;
clone.style.borderRadius = getComputedStyle(chip).borderRadius;
clone.style.padding = getComputedStyle(chip).padding;
clone.style.margin = '0'; // Remove any margin
document.body.appendChild(clone);
touchState.value.clone = clone;
chip.style.visibility = 'hidden';
event.preventDefault();
event.stopPropagation();
}
function handleTouchMove(event: TouchEvent): void {
if (!touchState.value.isDragging || !touchState.value.clone || event.touches.length > 1) return;
const touch = event.touches[0];
const clone = touchState.value.clone;
const dx = Math.abs(touch.clientX - touchState.value.startX);
const dy = Math.abs(touch.clientY - touchState.value.startY);
if (dx > 5 || dy > 5) {
touchState.value.hasMoved = true;
}
clone.style.left = `${touch.clientX - clone.offsetWidth / 2}px`;
clone.style.top = `${touch.clientY - clone.offsetHeight / 2}px`;
document.querySelectorAll('.group-box').forEach(el => {
el.classList.remove('highlight');
});
const elements = document.elementsFromPoint(touch.clientX, touch.clientY);
const dropTarget = elements.find(el => el.classList.contains('group-box'));
if (dropTarget) {
dropTarget.classList.add('highlight');
}
event.preventDefault();
event.stopPropagation();
}
function handleTouchEnd(event: TouchEvent): void {
if (!touchState.value.isDragging) return;
const {
currentGroupIndex,
currentStudentIndex,
clone,
element,
hasMoved
} = touchState.value;
document.querySelectorAll('.group-box').forEach(el => {
el.classList.remove('highlight');
});
if (clone?.parentNode) {
clone.parentNode.removeChild(clone);
}
if (element) {
element.style.visibility = 'visible';
}
if (hasMoved && event.changedTouches.length > 0) {
const touch = event.changedTouches[0];
const elements = document.elementsFromPoint(touch.clientX, touch.clientY);
const dropTarget = elements.find(el => el.classList.contains('group-box'));
if (dropTarget) {
const groupBoxes = document.querySelectorAll('.group-box');
const targetGroupIndex = Array.from(groupBoxes).indexOf(dropTarget);
if (targetGroupIndex !== currentGroupIndex) {
const sourceArray = currentGroupIndex === -1
? unassignedStudents.value
: currentGroups.value[currentGroupIndex];
const targetArray = targetGroupIndex === -1
? unassignedStudents.value
: currentGroups.value[targetGroupIndex];
if (sourceArray && targetArray) {
const [movedStudent] = sourceArray.splice(currentStudentIndex, 1);
targetArray.push(movedStudent);
}
// Initialize groups if they exist
if (existingGroups && existingGroups.length > 0) {
currentGroups.value = existingGroups.map((group) =>
group.members.map((member) => ({
username: member.username,
fullName: `${member.firstName} ${member.lastName}`,
})),
);
const assignedUsernames = new Set(
existingGroups.flatMap((g) => g.members.map((m: StudentItem) => m.username)),
);
unassignedStudents.value = allStudents.value.filter((s) => !assignedUsernames.has(s.username));
} else {
currentGroups.value = [];
unassignedStudents.value = [...allStudents.value];
}
randomGroupsPreview.value = [...currentGroups.value];
},
{ immediate: true },
);
/** Random groups functions */
function generateRandomGroups(): void {
if (groupSize.value < 1) return;
// Shuffle students
const shuffled = [...allStudents.value].sort(() => Math.random() - 0.5);
// Create new groups
const newGroups: StudentItem[][] = [];
const groupCount = Math.ceil(shuffled.length / groupSize.value);
for (let i = 0; i < groupCount; i++) {
newGroups.push([]);
}
// Distribute students
shuffled.forEach((student, index) => {
const groupIndex = index % groupCount;
newGroups[groupIndex].push(student);
});
randomGroupsPreview.value = newGroups;
}
touchState.value = {
function saveRandomGroups(): void {
if (randomGroupsPreview.value.length === 0) {
alert(t("please-generate-groups-first"));
return;
}
emit(
"groupsUpdated",
randomGroupsPreview.value.map((g) => g.map((s) => s.username)),
);
activeDialog.value = null;
emit("done");
emit("close");
}
function addNewGroup() {
currentGroups.value.push([]);
}
function removeGroup(index: number) {
// Move students back to unassigned
unassignedStudents.value.push(...currentGroups.value[index]);
currentGroups.value.splice(index, 1);
}
/** Drag and drop functions */
// Touch state interface
interface TouchState {
isDragging: boolean;
startX: number;
startY: number;
currentGroupIndex: number;
currentStudentIndex: number;
element: HTMLElement | null;
clone: HTMLElement | null;
originalRect: DOMRect | null;
hasMoved: boolean;
}
const touchState = ref<TouchState>({
isDragging: false,
startX: 0,
startY: 0,
@ -284,80 +138,217 @@ function handleTouchEnd(event: TouchEvent): void {
element: null,
clone: null,
originalRect: null,
hasMoved: false
};
hasMoved: false,
});
event.preventDefault();
event.stopPropagation();
}
function handleTouchStart(event: TouchEvent, groupIndex: number, studentIndex: number): void {
if (event.touches.length > 1) return;
function handleDragStart(event: DragEvent, groupIndex: number, studentIndex: number): void {
draggedItem.value = {groupIndex, studentIndex};
if (event.dataTransfer) {
event.dataTransfer.effectAllowed = 'move';
event.dataTransfer.setData('text/plain', '');
}
}
const touch = event.touches[0];
const target = event.target as HTMLElement;
// Target the chip directly instead of the draggable container
const chip = target.closest(".v-chip") as HTMLElement;
function handleDragOver(e: DragEvent, _: number): void {
e.preventDefault();
if (e.dataTransfer) {
e.dataTransfer.dropEffect = "move";
}
}
if (!chip) return;
function handleDrop(e: DragEvent, targetGroupIndex: number, targetStudentIndex?: number): void {
e.preventDefault();
if (!draggedItem.value) return;
// Get the chip's position relative to the viewport
const rect = chip.getBoundingClientRect();
const {groupIndex: sourceGroupIndex, studentIndex: sourceStudentIndex} = draggedItem.value;
const sourceArray = sourceGroupIndex === -1
? unassignedStudents.value
: currentGroups.value[sourceGroupIndex];
const targetArray = targetGroupIndex === -1
? unassignedStudents.value
: currentGroups.value[targetGroupIndex];
touchState.value = {
isDragging: true,
startX: touch.clientX,
startY: touch.clientY,
currentGroupIndex: groupIndex,
currentStudentIndex: studentIndex,
element: chip,
clone: null,
originalRect: rect,
hasMoved: false,
};
const [movedStudent] = sourceArray.splice(sourceStudentIndex, 1);
if (targetStudentIndex !== undefined) {
targetArray.splice(targetStudentIndex, 0, movedStudent);
} else {
targetArray.push(movedStudent);
// Clone only the chip
const clone = chip.cloneNode(true) as HTMLElement;
clone.classList.add("drag-clone");
clone.style.position = "fixed";
clone.style.zIndex = "10000";
clone.style.opacity = "0.9";
clone.style.pointerEvents = "none";
clone.style.width = `${rect.width}px`;
clone.style.height = `${rect.height}px`;
clone.style.left = `${rect.left}px`;
clone.style.top = `${rect.top}px`;
clone.style.transform = "scale(1.05)";
clone.style.boxShadow = "0 4px 8px rgba(0,0,0,0.3)";
clone.style.transition = "transform 0.1s";
// Ensure the clone has the same chip styling
clone.style.backgroundColor = getComputedStyle(chip).backgroundColor;
clone.style.color = getComputedStyle(chip).color;
clone.style.borderRadius = getComputedStyle(chip).borderRadius;
clone.style.padding = getComputedStyle(chip).padding;
clone.style.margin = "0"; // Remove any margin
document.body.appendChild(clone);
touchState.value.clone = clone;
chip.style.visibility = "hidden";
event.preventDefault();
event.stopPropagation();
}
draggedItem.value = null;
}
function handleTouchMove(event: TouchEvent): void {
if (!touchState.value.isDragging || !touchState.value.clone || event.touches.length > 1) return;
function saveDragDrop(): void {
if (unassignedStudents.value.length > 0) {
alert(t("please-assign-all-students"));
return;
const touch = event.touches[0];
const clone = touchState.value.clone;
const dx = Math.abs(touch.clientX - touchState.value.startX);
const dy = Math.abs(touch.clientY - touchState.value.startY);
if (dx > 5 || dy > 5) {
touchState.value.hasMoved = true;
}
clone.style.left = `${touch.clientX - clone.offsetWidth / 2}px`;
clone.style.top = `${touch.clientY - clone.offsetHeight / 2}px`;
document.querySelectorAll(".group-box").forEach((el) => {
el.classList.remove("highlight");
});
const elements = document.elementsFromPoint(touch.clientX, touch.clientY);
const dropTarget = elements.find((el) => el.classList.contains("group-box"));
if (dropTarget) {
dropTarget.classList.add("highlight");
}
event.preventDefault();
event.stopPropagation();
}
emit(
"groupsUpdated",
currentGroups.value.map((g) => g.map((s) => s.username)),
);
activeDialog.value = null;
emit("done");
emit("close");
}
function handleTouchEnd(event: TouchEvent): void {
if (!touchState.value.isDragging) return;
const showGroupsPreview = computed(() => {
return currentGroups.value.length > 0 || unassignedStudents.value.length > 0;
});
const { currentGroupIndex, currentStudentIndex, clone, element, hasMoved } = touchState.value;
function removeStudent(groupIndex: number, student: StudentItem): void {
const group = currentGroups.value[groupIndex];
currentGroups.value[groupIndex] = group.filter((s) => s.username !== student.username);
unassignedStudents.value.push(student);
}
document.querySelectorAll(".group-box").forEach((el) => {
el.classList.remove("highlight");
});
if (clone?.parentNode) {
clone.parentNode.removeChild(clone);
}
if (element) {
element.style.visibility = "visible";
}
if (hasMoved && event.changedTouches.length > 0) {
const touch = event.changedTouches[0];
const elements = document.elementsFromPoint(touch.clientX, touch.clientY);
const dropTarget = elements.find((el) => el.classList.contains("group-box"));
if (dropTarget) {
const groupBoxes = document.querySelectorAll(".group-box");
const targetGroupIndex = Array.from(groupBoxes).indexOf(dropTarget);
if (targetGroupIndex !== currentGroupIndex) {
const sourceArray =
currentGroupIndex === -1 ? unassignedStudents.value : currentGroups.value[currentGroupIndex];
const targetArray =
targetGroupIndex === -1 ? unassignedStudents.value : currentGroups.value[targetGroupIndex];
if (sourceArray && targetArray) {
const [movedStudent] = sourceArray.splice(currentStudentIndex, 1);
targetArray.push(movedStudent);
}
}
}
}
touchState.value = {
isDragging: false,
startX: 0,
startY: 0,
currentGroupIndex: -1,
currentStudentIndex: -1,
element: null,
clone: null,
originalRect: null,
hasMoved: false,
};
event.preventDefault();
event.stopPropagation();
}
function handleDragStart(event: DragEvent, groupIndex: number, studentIndex: number): void {
draggedItem.value = { groupIndex, studentIndex };
if (event.dataTransfer) {
event.dataTransfer.effectAllowed = "move";
event.dataTransfer.setData("text/plain", "");
}
}
function handleDragOver(e: DragEvent, _: number): void {
e.preventDefault();
if (e.dataTransfer) {
e.dataTransfer.dropEffect = "move";
}
}
function handleDrop(e: DragEvent, targetGroupIndex: number, targetStudentIndex?: number): void {
e.preventDefault();
if (!draggedItem.value) return;
const { groupIndex: sourceGroupIndex, studentIndex: sourceStudentIndex } = draggedItem.value;
const sourceArray = sourceGroupIndex === -1 ? unassignedStudents.value : currentGroups.value[sourceGroupIndex];
const targetArray = targetGroupIndex === -1 ? unassignedStudents.value : currentGroups.value[targetGroupIndex];
const [movedStudent] = sourceArray.splice(sourceStudentIndex, 1);
if (targetStudentIndex !== undefined) {
targetArray.splice(targetStudentIndex, 0, movedStudent);
} else {
targetArray.push(movedStudent);
}
draggedItem.value = null;
}
function saveDragDrop(): void {
if (unassignedStudents.value.length > 0) {
alert(t("please-assign-all-students"));
return;
}
emit(
"groupsUpdated",
currentGroups.value.map((g) => g.map((s) => s.username)),
);
activeDialog.value = null;
emit("done");
emit("close");
}
const showGroupsPreview = computed(() => {
return currentGroups.value.length > 0 || unassignedStudents.value.length > 0;
});
function removeStudent(groupIndex: number, student: StudentItem): void {
const group = currentGroups.value[groupIndex];
currentGroups.value[groupIndex] = group.filter((s) => s.username !== student.username);
unassignedStudents.value.push(student);
}
</script>
<template>
<v-card class="pa-4 minimal-card">
<!-- Current groups and unassigned students Preview -->
<div v-if="showGroupsPreview" class="mb-6">
<div
v-if="showGroupsPreview"
class="mb-6"
>
<h3 class="mb-2">{{ t("current-groups") }}</h3>
<div>
<div class="d-flex flex-wrap">
@ -366,11 +357,23 @@ function removeStudent(groupIndex: number, student: StudentItem): void {
</div>
</div>
<v-row justify="center" class="mb-4">
<v-btn color="primary" @click="activeDialog = 'random'" prepend-icon="mdi-shuffle">
<v-row
justify="center"
class="mb-4"
>
<v-btn
color="primary"
@click="activeDialog = 'random'"
prepend-icon="mdi-shuffle"
>
{{ t("random-grouping") }}
</v-btn>
<v-btn color="secondary" class="ml-4" @click="activeDialog = 'dragdrop'" prepend-icon="mdi-drag">
<v-btn
color="secondary"
class="ml-4"
@click="activeDialog = 'dragdrop'"
prepend-icon="mdi-drag"
>
{{ t("drag-and-drop") }}
</v-btn>
</v-row>
@ -436,8 +439,12 @@ function removeStudent(groupIndex: number, student: StudentItem): void {
</v-card-text>
<v-card-actions class="dialog-actions">
<v-spacer/>
<v-btn text @click="activeDialog = null">{{ t("cancel") }}</v-btn>
<v-spacer />
<v-btn
text
@click="activeDialog = null"
>{{ t("cancel") }}</v-btn
>
<v-btn
color="success"
@click="saveRandomGroups"
@ -456,16 +463,27 @@ function removeStudent(groupIndex: number, student: StudentItem): void {
max-width="900"
>
<v-card class="custom-dialog">
<v-card-title class=" dialog-title d-flex justify-space-between align-center">
<v-card-title class="dialog-title d-flex justify-space-between align-center">
<div>{{ t("drag-and-drop") }}</div>
<v-btn color="primary" small @click="addNewGroup">+</v-btn>
<v-btn
color="primary"
small
@click="addNewGroup"
>+</v-btn
>
</v-card-title>
<v-card-text>
<v-row>
<!-- Groups Column -->
<v-col cols="12" md="8">
<div v-if="currentGroups.length === 0" class="text-center py-4">
<v-col
cols="12"
md="8"
>
<div
v-if="currentGroups.length === 0"
class="text-center py-4"
>
<v-alert type="info">{{ t("no-groups-yet") }}</v-alert>
</div>
@ -479,7 +497,12 @@ function removeStudent(groupIndex: number, student: StudentItem): void {
>
<div class="d-flex justify-space-between align-center mb-2">
<strong>{{ t("group") }} {{ groupIndex + 1 }}</strong>
<v-btn icon small color="error" @click="removeGroup(groupIndex)">
<v-btn
icon
small
color="error"
@click="removeGroup(groupIndex)"
>
<v-icon>mdi-delete</v-icon>
</v-btn>
</div>
@ -497,7 +520,10 @@ function removeStudent(groupIndex: number, student: StudentItem): void {
@dragover.prevent="handleDragOver($event, groupIndex)"
@drop="handleDrop($event, groupIndex, studentIndex)"
>
<v-chip close @click:close="removeStudent(groupIndex, student)">
<v-chip
close
@click:close="removeStudent(groupIndex, student)"
>
{{ student.fullName }}
</v-chip>
</div>
@ -539,8 +565,12 @@ function removeStudent(groupIndex: number, student: StudentItem): void {
</v-card-text>
<v-card-actions>
<v-spacer/>
<v-btn text @click="activeDialog = null">{{ t("cancel") }}</v-btn>
<v-spacer />
<v-btn
text
@click="activeDialog = null"
>{{ t("cancel") }}</v-btn
>
<v-btn
color="primary"
@click="saveDragDrop"
@ -555,101 +585,101 @@ function removeStudent(groupIndex: number, student: StudentItem): void {
</template>
<style scoped>
.group-box {
min-height: 100px;
max-height: 200px;
overflow-y: auto;
background-color: #fafafa;
border-radius: 4px;
transition: all 0.2s;
}
.group-box {
min-height: 100px;
max-height: 200px;
overflow-y: auto;
background-color: #fafafa;
border-radius: 4px;
transition: all 0.2s;
}
.group-box.highlight {
background-color: #e3f2fd;
border: 2px dashed #2196f3;
}
.group-box.highlight {
background-color: #e3f2fd;
border: 2px dashed #2196f3;
}
.v-expansion-panel-text {
max-height: 200px;
overflow-y: auto;
}
.v-expansion-panel-text {
max-height: 200px;
overflow-y: auto;
}
.drag-clone {
z-index: 10000;
transform: scale(1.05);
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.3);
transition: transform 0.1s;
will-change: transform;
pointer-events: none;
display: inline-flex;
align-items: center;
justify-content: center;
border-radius: 16px;
background-color: inherit;
}
.drag-clone {
z-index: 10000;
transform: scale(1.05);
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.3);
transition: transform 0.1s;
will-change: transform;
pointer-events: none;
display: inline-flex;
align-items: center;
justify-content: center;
border-radius: 16px;
background-color: inherit;
}
.draggable-item {
display: inline-block;
}
.draggable-item {
display: inline-block;
}
.draggable-item .v-chip[style*="hidden"] {
visibility: hidden;
display: inline-block;
}
.draggable-item .v-chip[style*="hidden"] {
visibility: hidden;
display: inline-block;
}
.custom-dialog {
border-radius: 16px;
padding: 24px;
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.15);
}
.custom-dialog {
border-radius: 16px;
padding: 24px;
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.15);
}
.dialog-title {
color: #00796b; /* teal-like green */
font-weight: bold;
font-size: 1.25rem;
margin-bottom: 16px;
}
.dialog-title {
color: #00796b; /* teal-like green */
font-weight: bold;
font-size: 1.25rem;
margin-bottom: 16px;
}
.dialog-actions {
display: flex;
justify-content: flex-end;
gap: 12px;
margin-top: 24px;
}
.dialog-actions {
display: flex;
justify-content: flex-end;
gap: 12px;
margin-top: 24px;
}
.v-btn.custom-green {
background-color: #43a047;
color: white;
}
.v-btn.custom-green {
background-color: #43a047;
color: white;
}
.v-btn.custom-green:hover {
background-color: #388e3c
}
.v-btn.custom-green:hover {
background-color: #388e3c;
}
.v-btn.custom-blue {
background-color: #1e88e5;
color: white;
}
.v-btn.custom-blue {
background-color: #1e88e5;
color: white;
}
.v-btn.custom-blue:hover {
background-color: #1565c0 ;
}
.v-btn.custom-blue:hover {
background-color: #1565c0;
}
.v-btn.cancel-button {
background-color: #e0f2f1;
color: #00695c;
}
.v-btn.cancel-button {
background-color: #e0f2f1;
color: #00695c;
}
.minimal-card {
box-shadow: none; /* remove card shadow */
border: none; /* remove border */
background-color: transparent; /* make background transparent */
padding: 0; /* reduce padding */
margin-bottom: 1rem; /* keep some spacing below */
}
.minimal-card {
box-shadow: none; /* remove card shadow */
border: none; /* remove border */
background-color: transparent; /* make background transparent */
padding: 0; /* reduce padding */
margin-bottom: 1rem; /* keep some spacing below */
}
/* Optionally, keep some padding only around buttons */
.minimal-card > .v-row {
padding: 1rem 0; /* give vertical padding around buttons */
}
/* Optionally, keep some padding only around buttons */
.minimal-card > .v-row {
padding: 1rem 0; /* give vertical padding around buttons */
}
</style>

View file

@ -1,11 +1,9 @@
/**
* Validation rule for the assignment title.
*
* Ensures that the title is not empty.
*/
/**
* Validation rule for the classes selection.
*
@ -17,4 +15,3 @@
*
* Ensures that a valid deadline is selected and is in the future.
*/

View file

@ -1,119 +1,114 @@
<script setup lang="ts">
import { useI18n } from "vue-i18n";
import { computed, onMounted, ref, watch } from "vue";
import auth from "@/services/auth/auth-service.ts";
import { useTeacherClassesQuery } from "@/queries/teachers.ts";
import { useRouter, useRoute } 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 { useI18n } from "vue-i18n";
import { computed, onMounted, ref, watch } from "vue";
import auth from "@/services/auth/auth-service.ts";
import { useTeacherClassesQuery } from "@/queries/teachers.ts";
import { useRouter, useRoute } 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";
const route = useRoute();
const router = useRouter();
const { t, locale } = useI18n();
const role = ref(auth.authState.activeRole);
const username = ref<string>("");
const route = useRoute();
const router = useRouter();
const { t, locale } = useI18n();
const role = ref(auth.authState.activeRole);
const username = ref<string>("");
onMounted(async () => {
if (role.value === "student") {
await router.push("/user");
}
const user = await auth.loadUser();
username.value = user?.profile?.preferred_username ?? "";
});
const language = computed(() => locale.value);
const form = ref();
const learningPathsQueryResults = useGetAllLearningPaths(language);
const classesQueryResults = useTeacherClassesQuery(username, true);
const selectedClass = ref(undefined);
const assignmentTitle = ref("");
const selectedLearningPath = ref<LearningPath | undefined>(undefined);
const lpIsSelected = ref(false);
watch(learningPathsQueryResults.data, (data) => {
const hruidFromRoute = route.query.hruid?.toString();
if (!hruidFromRoute || !data) return;
// Verify if the hruid given in the url is valid before accepting it
const matchedLP = data.find(lp => lp.hruid === hruidFromRoute);
if (matchedLP) {
selectedLearningPath.value = matchedLP;
lpIsSelected.value = true;
}
});
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;
const lp = lpIsSelected.value
? route.query.hruid?.toString()
: selectedLearningPath.value?.hruid;
if (!lp) {
return;
}
const assignmentDTO: AssignmentDTO = {
id: 0,
within: selectedClass.value?.id || "",
title: assignmentTitle.value,
description: "",
learningPath: lp,
language: language.value,
deadline: null,
groups: [],
};
mutate({ cid: assignmentDTO.within, data: assignmentDTO });
}
const learningPathRules = [
(value: LearningPath): string | boolean => {
if(lpIsSelected.value) return true;
if (!value) return t("lp-required");
const allLPs = learningPathsQueryResults.data.value ?? [];
const valid = allLPs.some(lp => lp.hruid === value?.hruid);
return valid || t("lp-invalid");
}
];
const assignmentTitleRules = [
(value: string): string | boolean => {
if (value?.length >= 1) {
return true;
} // Title must not be empty
return t("title-required");
},
];
const classRules = [
(value: string): string | boolean => {
if (value) {
return true;
onMounted(async () => {
if (role.value === "student") {
await router.push("/user");
}
return t("class-required");
},
];
const user = await auth.loadUser();
username.value = user?.profile?.preferred_username ?? "";
});
const language = computed(() => locale.value);
const form = ref();
const learningPathsQueryResults = useGetAllLearningPaths(language);
const classesQueryResults = useTeacherClassesQuery(username, true);
const selectedClass = ref(undefined);
const assignmentTitle = ref("");
const selectedLearningPath = ref<LearningPath | undefined>(undefined);
const lpIsSelected = ref(false);
watch(learningPathsQueryResults.data, (data) => {
const hruidFromRoute = route.query.hruid?.toString();
if (!hruidFromRoute || !data) return;
// Verify if the hruid given in the url is valid before accepting it
const matchedLP = data.find((lp) => lp.hruid === hruidFromRoute);
if (matchedLP) {
selectedLearningPath.value = matchedLP;
lpIsSelected.value = true;
}
});
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;
const lp = lpIsSelected.value ? route.query.hruid?.toString() : selectedLearningPath.value?.hruid;
if (!lp) {
return;
}
const assignmentDTO: AssignmentDTO = {
id: 0,
within: selectedClass.value?.id || "",
title: assignmentTitle.value,
description: "",
learningPath: lp,
language: language.value,
deadline: null,
groups: [],
};
mutate({ cid: assignmentDTO.within, data: assignmentDTO });
}
const learningPathRules = [
(value: LearningPath): string | boolean => {
if (lpIsSelected.value) return true;
if (!value) return t("lp-required");
const allLPs = learningPathsQueryResults.data.value ?? [];
const valid = allLPs.some((lp) => lp.hruid === value?.hruid);
return valid || t("lp-invalid");
},
];
const assignmentTitleRules = [
(value: string): string | boolean => {
if (value?.length >= 1) {
return true;
} // Title must not be empty
return t("title-required");
},
];
const classRules = [
(value: string): string | boolean => {
if (value) {
return true;
}
return t("class-required");
},
];
</script>
<template>
@ -156,7 +151,6 @@ const classRules = [
:disabled="lpIsSelected"
return-object
/>
</using-query-result>
<!-- Klas keuze -->
@ -212,59 +206,59 @@ const classRules = [
</template>
<style scoped>
.main-container {
display: flex;
flex-direction: column;
align-items: center;
justify-content: start;
padding-top: 32px;
text-align: center;
}
.main-container {
display: flex;
flex-direction: column;
align-items: center;
justify-content: start;
padding-top: 32px;
text-align: center;
}
.form-card {
width: 100%;
max-width: 720px;
border-radius: 16px;
}
.form-container {
display: flex;
flex-direction: column;
gap: 24px;
width: 100%;
}
.step-container {
display: flex;
flex-direction: column;
gap: 24px;
}
@media (max-width: 1000px) {
.form-card {
width: 85%;
padding: 1%;
width: 100%;
max-width: 720px;
border-radius: 16px;
}
}
@media (max-width: 600px) {
h1 {
font-size: 32px;
text-align: center;
margin-left: 0;
.form-container {
display: flex;
flex-direction: column;
gap: 24px;
width: 100%;
}
}
@media (max-width: 400px) {
h1 {
font-size: 24px;
text-align: center;
margin-left: 0;
.step-container {
display: flex;
flex-direction: column;
gap: 24px;
}
}
.v-card {
border: 2px solid #0e6942;
border-radius: 12px;
}
@media (max-width: 1000px) {
.form-card {
width: 85%;
padding: 1%;
}
}
@media (max-width: 600px) {
h1 {
font-size: 32px;
text-align: center;
margin-left: 0;
}
}
@media (max-width: 400px) {
h1 {
font-size: 24px;
text-align: center;
margin-left: 0;
}
}
.v-card {
border: 2px solid #0e6942;
border-radius: 12px;
}
</style>

View file

@ -6,11 +6,11 @@
import UsingQueryResult from "@/components/UsingQueryResult.vue";
import type { AssignmentResponse } from "@/controllers/assignments.ts";
import { asyncComputed } from "@vueuse/core";
import {useStudentGroupsQuery, useStudentsByUsernamesQuery} from "@/queries/students.ts";
import { useStudentGroupsQuery, useStudentsByUsernamesQuery } from "@/queries/students.ts";
import { useGetLearningPathQuery } from "@/queries/learning-paths.ts";
import type { Language } from "@/data-objects/language.ts";
import { calculateProgress } from "@/utils/assignment-utils.ts";
import type {LearningPath} from "@/data-objects/learning-paths/learning-path.ts";
import type { LearningPath } from "@/data-objects/learning-paths/learning-path.ts";
const props = defineProps<{
classId: string;

View file

@ -1,159 +1,156 @@
<script setup lang="ts">
import {computed, ref, watch, watchEffect} from "vue";
import {useI18n} from "vue-i18n";
import {
useAssignmentQuery,
useDeleteAssignmentMutation,
useUpdateAssignmentMutation,
} 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, GroupDTOId} from "@dwengo-1/common/interfaces/group";
import GroupSubmissionStatus from "@/components/GroupSubmissionStatus.vue";
import GroupProgressRow from "@/components/GroupProgressRow.vue";
import type {AssignmentDTO} from "@dwengo-1/common/dist/interfaces/assignment.ts";
import GroupSelector from "@/components/assignments/GroupSelector.vue";
import DeadlineSelector from "@/components/assignments/DeadlineSelector.vue";
import { computed, ref, watch, watchEffect } from "vue";
import { useI18n } from "vue-i18n";
import {
useAssignmentQuery,
useDeleteAssignmentMutation,
useUpdateAssignmentMutation,
} 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, GroupDTOId } from "@dwengo-1/common/interfaces/group";
import GroupSubmissionStatus from "@/components/GroupSubmissionStatus.vue";
import GroupProgressRow from "@/components/GroupProgressRow.vue";
import type { AssignmentDTO } from "@dwengo-1/common/dist/interfaces/assignment.ts";
import GroupSelector from "@/components/assignments/GroupSelector.vue";
import DeadlineSelector from "@/components/assignments/DeadlineSelector.vue";
const props = defineProps<{
classId: string;
assignmentId: number;
}>();
const props = defineProps<{
classId: string;
assignmentId: number;
}>();
const isEditing = ref(false);
const isEditing = ref(false);
const {t} = useI18n();
const lang = ref();
const groups = ref<GroupDTO[] | GroupDTOId[]>([]);
const learningPath = ref();
const form = ref();
const { t } = useI18n();
const lang = ref();
const groups = ref<GroupDTO[] | GroupDTOId[]>([]);
const learningPath = ref();
const form = ref();
const editingLearningPath = ref(learningPath);
const description = ref("");
const deadline = ref<Date | null>(null);
const editGroups = ref(false);
const editingLearningPath = ref(learningPath);
const description = ref("");
const deadline = ref<Date | null>(null);
const editGroups = ref(false);
const assignmentQueryResult = useAssignmentQuery(() => props.classId, props.assignmentId);
// Get learning path object
const lpQueryResult = useGetLearningPathQuery(
computed(() => assignmentQueryResult.data.value?.assignment?.learningPath ?? ""),
computed(() => assignmentQueryResult.data.value?.assignment?.language as Language),
);
// Get all the groups withing the assignment
const groupsQueryResult = useGroupsQuery(props.classId, props.assignmentId, true);
groups.value = groupsQueryResult.data.value?.groups ?? [];
watchEffect(() => {
const assignment = assignmentQueryResult.data.value?.assignment;
if (assignment) {
learningPath.value = assignment.learningPath;
lang.value = assignment.language as Language;
deadline.value = assignment.deadline ? new Date(assignment.deadline) : null;
if (lpQueryResult.data.value) {
editingLearningPath.value = lpQueryResult.data.value;
}
}
});
const hasSubmissions = ref<boolean>(false);
const allGroups = computed(() => {
const groups = groupsQueryResult.data.value?.groups;
if (!groups) return [];
// Sort by original groupNumber
const sortedGroups = [...groups].sort((a, b) => a.groupNumber - b.groupNumber);
// Assign new sequential numbers starting from 1
return sortedGroups.map((group, index) => ({
groupNo: index + 1, // New group number that will be used
name: `${t("group")} ${index + 1}`,
members: group.members,
originalGroupNo: group.groupNumber
}));
});
const dialog = ref(false);
const selectedGroup = ref({});
function openGroupDetails(group: object): void {
selectedGroup.value = group;
dialog.value = true;
}
async function deleteAssignment(num: number, clsId: string): Promise<void> {
const {mutate} = useDeleteAssignmentMutation();
mutate(
{cid: clsId, an: num},
{
onSuccess: () => {
window.location.href = "/user/assignment";
},
},
const assignmentQueryResult = useAssignmentQuery(() => props.classId, props.assignmentId);
// Get learning path object
const lpQueryResult = useGetLearningPathQuery(
computed(() => assignmentQueryResult.data.value?.assignment?.learningPath ?? ""),
computed(() => assignmentQueryResult.data.value?.assignment?.language as Language),
);
}
function goToLearningPathLink(): string | undefined {
const assignment = assignmentQueryResult.data.value?.assignment;
const lp = lpQueryResult.data.value;
// Get all the groups withing the assignment
const groupsQueryResult = useGroupsQuery(props.classId, props.assignmentId, true);
groups.value = groupsQueryResult.data.value?.groups ?? [];
if (!assignment || !lp) return undefined;
watchEffect(() => {
const assignment = assignmentQueryResult.data.value?.assignment;
if (assignment) {
learningPath.value = assignment.learningPath;
lang.value = assignment.language as Language;
deadline.value = assignment.deadline ? new Date(assignment.deadline) : null;
return `/learningPath/${lp.hruid}/${assignment.language}/${lp.startNode.learningobjectHruid}?assignmentNo=${props.assignmentId}&classId=${props.classId}`;
}
if (lpQueryResult.data.value) {
editingLearningPath.value = lpQueryResult.data.value;
}
}
});
function goToGroupSubmissionLink(groupNo: number): string | undefined {
const lp = lpQueryResult.data.value;
if (!lp) return undefined;
const hasSubmissions = ref<boolean>(false);
return `/learningPath/${lp.hruid}/${lp.language}/${lp.startNode.learningobjectHruid}?forGroup=${groupNo}&assignmentNo=${props.assignmentId}&classId=${props.classId}`;
}
const allGroups = computed(() => {
const groups = groupsQueryResult.data.value?.groups;
const {mutate, data, isSuccess} = useUpdateAssignmentMutation();
if (!groups) return [];
watch([isSuccess, data], async ([success, newData]) => {
if (success && newData?.assignment) {
await assignmentQueryResult.refetch();
// Sort by original groupNumber
const sortedGroups = [...groups].sort((a, b) => a.groupNumber - b.groupNumber);
// Assign new sequential numbers starting from 1
return sortedGroups.map((group, index) => ({
groupNo: index + 1, // New group number that will be used
name: `${t("group")} ${index + 1}`,
members: group.members,
originalGroupNo: group.groupNumber,
}));
});
const dialog = ref(false);
const selectedGroup = ref({});
function openGroupDetails(group: object): void {
selectedGroup.value = group;
dialog.value = true;
}
});
async function saveChanges(): Promise<void> {
const {valid} = await form.value.validate();
if (!valid) return;
async function deleteAssignment(num: number, clsId: string): Promise<void> {
const { mutate } = useDeleteAssignmentMutation();
mutate(
{ cid: clsId, an: num },
{
onSuccess: () => {
window.location.href = "/user/assignment";
},
},
);
}
isEditing.value = false;
function goToLearningPathLink(): string | undefined {
const assignment = assignmentQueryResult.data.value?.assignment;
const lp = lpQueryResult.data.value;
const assignmentDTO: AssignmentDTO = {
description: description.value,
deadline: deadline.value ?? null,
};
if (!assignment || !lp) return undefined;
mutate({
cid: assignmentQueryResult.data.value?.assignment.within,
an: assignmentQueryResult.data.value?.assignment.id,
data: assignmentDTO,
return `/learningPath/${lp.hruid}/${assignment.language}/${lp.startNode.learningobjectHruid}?assignmentNo=${props.assignmentId}&classId=${props.classId}`;
}
function goToGroupSubmissionLink(groupNo: number): string | undefined {
const lp = lpQueryResult.data.value;
if (!lp) return undefined;
return `/learningPath/${lp.hruid}/${lp.language}/${lp.startNode.learningobjectHruid}?forGroup=${groupNo}&assignmentNo=${props.assignmentId}&classId=${props.classId}`;
}
const { mutate, data, isSuccess } = useUpdateAssignmentMutation();
watch([isSuccess, data], async ([success, newData]) => {
if (success && newData?.assignment) {
await assignmentQueryResult.refetch();
}
});
}
async function handleGroupsUpdated(updatedGroups: string[][]): Promise<void> {
const assignmentDTO: AssignmentDTO = {
groups: updatedGroups,
};
mutate({
cid: assignmentQueryResult.data.value?.assignment.within,
an: assignmentQueryResult.data.value?.assignment.id,
data: assignmentDTO,
});
}
async function saveChanges(): Promise<void> {
const { valid } = await form.value.validate();
if (!valid) return;
isEditing.value = false;
const assignmentDTO: AssignmentDTO = {
description: description.value,
deadline: deadline.value ?? null,
};
mutate({
cid: assignmentQueryResult.data.value?.assignment.within,
an: assignmentQueryResult.data.value?.assignment.id,
data: assignmentDTO,
});
}
async function handleGroupsUpdated(updatedGroups: string[][]): Promise<void> {
const assignmentDTO: AssignmentDTO = {
groups: updatedGroups,
};
mutate({
cid: assignmentQueryResult.data.value?.assignment.within,
an: assignmentQueryResult.data.value?.assignment.id,
data: assignmentDTO,
});
}
</script>
<template>
@ -220,7 +217,7 @@ async function handleGroupsUpdated(updatedGroups: string[][]): Promise<void> {
editingLearningPath = learningPath;
}
"
>{{ t("cancel") }}
>{{ t("cancel") }}
</v-btn>
<v-btn
@ -250,7 +247,7 @@ async function handleGroupsUpdated(updatedGroups: string[][]): Promise<void> {
</div>
</div>
<v-card-title class="text-h4 assignmentTopTitle"
>{{ assignmentResponse.data.assignment.title }}
>{{ assignmentResponse.data.assignment.title }}
</v-card-title>
<v-card-subtitle class="subtitle-section">
<using-query-result
@ -275,9 +272,7 @@ async function handleGroupsUpdated(updatedGroups: string[][]): Promise<void> {
</using-query-result>
</v-card-subtitle>
<v-card-text v-if="isEditing">
<deadline-selector
v-model:deadline="deadline"
/>
<deadline-selector v-model:deadline="deadline" />
</v-card-text>
<v-card-text
v-if="!isEditing"
@ -353,66 +348,65 @@ async function handleGroupsUpdated(updatedGroups: string[][]): Promise<void> {
<div class="table-container">
<v-table class="table">
<thead>
<tr>
<th class="header">{{ t("group") }}</th>
<th class="header">{{ t("progress") }}</th>
<th class="header">{{ t("submission") }}</th>
<th class="header">
<v-btn
@click="editGroups = true"
variant="text"
:disabled="hasSubmissions"
>
<v-icon>mdi-pencil</v-icon>
</v-btn>
</th>
</tr>
<tr>
<th class="header">{{ t("group") }}</th>
<th class="header">{{ t("progress") }}</th>
<th class="header">{{ t("submission") }}</th>
<th class="header">
<v-btn
@click="editGroups = true"
variant="text"
:disabled="hasSubmissions"
>
<v-icon>mdi-pencil</v-icon>
</v-btn>
</th>
</tr>
</thead>
<tbody>
<tr
v-for="g in allGroups"
:key="g.originalGroupNo"
>
<td>
<v-btn
variant="text"
>
{{ g.name }}
</v-btn>
</td>
<tr
v-for="g in allGroups"
:key="g.originalGroupNo"
>
<td>
<v-btn variant="text">
{{ g.name }}
</v-btn>
</td>
<td>
<GroupProgressRow
:group-number="g.originalGroupNo"
:learning-path="learningPath"
:language="lang"
:assignment-id="assignmentId"
:class-id="classId"
/>
</td>
<td>
<GroupProgressRow
:group-number="g.originalGroupNo"
:learning-path="learningPath"
:language="lang"
:assignment-id="assignmentId"
:class-id="classId"
/>
</td>
<td>
<GroupSubmissionStatus
:group="g"
:assignment-id="assignmentId"
:class-id="classId"
:language="lang"
:go-to-group-submission-link="goToGroupSubmissionLink"
@update:hasSubmission="(hasSubmission) => hasSubmissions = hasSubmission"
<td>
<GroupSubmissionStatus
:group="g"
:assignment-id="assignmentId"
:class-id="classId"
:language="lang"
:go-to-group-submission-link="goToGroupSubmissionLink"
@update:hasSubmission="
(hasSubmission) => (hasSubmissions = hasSubmission)
"
/>
</td>
/>
</td>
<!-- Edit icon -->
<td>
<v-btn
@click="openGroupDetails(g)"
variant="text"
>
<v-icon>mdi-eye</v-icon>
</v-btn>
</td>
</tr>
<!-- Edit icon -->
<td>
<v-btn
@click="openGroupDetails(g)"
variant="text"
>
<v-icon>mdi-eye</v-icon>
</v-btn>
</td>
</tr>
</tbody>
</v-table>
</div>
@ -438,148 +432,149 @@ async function handleGroupsUpdated(updatedGroups: string[][]): Promise<void> {
<v-card-actions>
<v-spacer></v-spacer>
<v-btn text @click="editGroups = false">
<v-btn
text
@click="editGroups = false"
>
{{ t("cancel") }}
</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
</v-container>
</using-query-result>
</div>
</template>
<style scoped>
@import "@/assets/assignment.css";
@import "@/assets/assignment.css";
.table-scroll {
overflow-x: auto;
-webkit-overflow-scrolling: touch;
}
.table-scroll {
overflow-x: auto;
-webkit-overflow-scrolling: touch;
}
.header {
font-weight: bold !important;
background-color: #0e6942;
color: white;
padding: 10px;
}
.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:first-child {
border-top-left-radius: 10px;
}
.table thead th:last-child {
border-top-right-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(odd) {
background-color: white;
}
.table tbody tr:nth-child(even) {
background-color: #f6faf2;
}
.table tbody tr:nth-child(even) {
background-color: #f6faf2;
}
td,
th {
border-bottom: 1px solid #0e6942;
border-top: 1px solid #0e6942;
}
td,
th {
border-bottom: 1px solid #0e6942;
border-top: 1px solid #0e6942;
}
.table {
width: 90%;
padding-top: 10px;
border-collapse: collapse;
}
.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;
}
.table-container {
width: 100%;
overflow-x: visible;
}
.table {
width: 100%;
min-width: auto;
table-layout: auto;
}
@media screen and (max-width: 850px) {
h1 {
text-align: center;
padding-left: 0;
color: #0e6942;
text-transform: uppercase;
font-weight: bolder;
padding-top: 2%;
font-size: 50px;
}
h2 {
color: #0e6942;
font-size: 30px;
}
.join {
text-align: center;
align-items: center;
margin-left: 0;
display: flex;
flex-direction: column;
gap: 20px;
margin-top: 50px;
}
.sheet {
width: 100%;
.link {
color: #0b75bb;
text-decoration: underline;
}
main {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
margin: 5px;
margin-left: 30px;
}
.custom-breakpoint {
flex-direction: column !important;
.table-container {
width: 100%;
overflow-x: visible;
}
.table {
width: 100%;
display: block;
overflow-x: auto;
min-width: auto;
table-layout: auto;
}
.table-container {
overflow-x: auto;
@media screen and (max-width: 850px) {
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;
}
.custom-breakpoint {
flex-direction: column !important;
}
.table {
width: 100%;
display: block;
overflow-x: auto;
}
.table-container {
overflow-x: auto;
}
.responsive-col {
max-width: 100% !important;
flex-basis: 100% !important;
}
}
.responsive-col {
max-width: 100% !important;
flex-basis: 100% !important;
.group-members-dialog {
max-height: 80vh;
overflow-y: auto;
}
}
.group-members-dialog {
max-height: 80vh;
overflow-y: auto;
}
</style>