Skip to content

Commit 1bb359a

Browse files
authored
Merge pull request #187 from MEITREX/FoPro_WS_UML-Assessment
Fo pro ws uml assessment
2 parents 83daa38 + e9a29d7 commit 1bb359a

36 files changed

Lines changed: 19252 additions & 488 deletions

app/courses/[courseId]/progress/page.tsx

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@ import {
1313
IconButton,
1414
Slide,
1515
Typography,
16-
useTheme,
1716
} from "@mui/material";
1817
import { useParams } from "next/navigation";
1918
import { useEffect, useMemo, useState } from "react";
@@ -127,7 +126,7 @@ export default function LearningProgress() {
127126

128127
const progressItem = progressBySkillValues.get(key)!;
129128

130-
progressItem.progressSum += skill.skillValue.skillValue;
129+
progressItem.progressSum += skill.skillValue?.skillValue ?? 0;
131130
/*progressItem.averageProgressSum +=
132131
skill.skillAllUsersStats.skillValueSum;*/
133132
progressItem.count++;
@@ -262,8 +261,6 @@ export default function LearningProgress() {
262261
sessionStorage.setItem("previousProgress", JSON.stringify([...tempMap]));
263262
}, [course.skills, progressBySkill, uniqueCategories.length]);
264263

265-
const theme = useTheme();
266-
267264
if (uniqueCategories.length === 0) {
268265
return (
269266
<Typography variant="body1">
@@ -395,7 +392,9 @@ export default function LearningProgress() {
395392
competencyName={category}
396393
startProgress={Math.floor(previousCategoryProgressValue)}
397394
endProgress={Math.floor(categoryProgressValue)}
398-
averageProgress={/*Math.floor(categoryAverageProgressValue)*/0}
395+
averageProgress={
396+
/*Math.floor(categoryAverageProgressValue)*/ 0
397+
}
399398
color={stringToColor(category)}
400399
onClick={() => {
401400
setSelectedCategory(
@@ -407,7 +406,7 @@ export default function LearningProgress() {
407406
isSelected={category === sortedCategories[selectedCategory]}
408407
isUrgent={urgent}
409408
showAverageProgress={showAverageProgress}
410-
participantCount={/*maxParticipantCountForaSkill*/0}
409+
participantCount={/*maxParticipantCountForaSkill*/ 0}
411410
courseMemberCount={course.numberOfCourseMemberships}
412411
openTaskCount={
413412
filteredSuggestionsByCategory(category).length
@@ -507,7 +506,9 @@ export default function LearningProgress() {
507506
small={true}
508507
startProgress={Math.floor(previousSkillProgressValue)}
509508
endProgress={Math.floor(skillProgressValue)}
510-
averageProgress={/*Math.floor(skillAverageProgressValue)*/0}
509+
averageProgress={
510+
/*Math.floor(skillAverageProgressValue)*/ 0
511+
}
511512
color={stringToColor(currentSkill.skillCategory)}
512513
onClick={() => {
513514
const currentIndex = currentUniqueSkills.findIndex(
@@ -525,7 +526,7 @@ export default function LearningProgress() {
525526
}
526527
isUrgent={urgent}
527528
showAverageProgress={showAverageProgress}
528-
participantCount={/*maxParticipantCount*/0}
529+
participantCount={/*maxParticipantCount*/ 0}
529530
courseMemberCount={course.numberOfCourseMemberships}
530531
openTaskCount={
531532
filteredSuggestionsBySkill(currentSkill.skillName).length
Lines changed: 192 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,192 @@
1+
"use client";
2+
3+
import EditIcon from "@mui/icons-material/Edit";
4+
import InfoIcon from "@mui/icons-material/Info";
5+
import PeopleIcon from "@mui/icons-material/People";
6+
import {
7+
Box,
8+
Button,
9+
CircularProgress,
10+
Snackbar,
11+
Stack,
12+
Tab,
13+
Tabs,
14+
} from "@mui/material";
15+
import { useParams } from "next/navigation";
16+
import { useMemo, useState } from "react";
17+
import { useLazyLoadQuery, useMutation } from "react-relay";
18+
19+
import {
20+
umlApiFindUserInfosQuery,
21+
umlApiGetLecturerExerciseOverviewQuery,
22+
umlApiUpdateTutorSolutionMutation,
23+
} from "@/components/hylimo/api/UmlApi";
24+
import { getSemanticModel } from "@/components/hylimo/semanticModelGenerator";
25+
import { AddUMLAssignmentModal } from "@/components/uml-assignment/AddUMLAssignmentModal";
26+
import ExerciseInfoTab from "@/components/uml-assignment/ExerciseInfoTab";
27+
import SubmissionsTab from "@/components/uml-assignment/SubmissionsTab";
28+
29+
export default function LecturerUmlAssignment() {
30+
const { umlId } = useParams();
31+
const [tabIndex, setTabIndex] = useState(0);
32+
const [isEditModalOpen, setIsEditModalOpen] = useState(false);
33+
const [refreshKey, setRefreshKey] = useState(0);
34+
const [snackbar, setSnackbar] = useState({ open: false, message: "" });
35+
36+
const data = useLazyLoadQuery<any>(
37+
umlApiGetLecturerExerciseOverviewQuery,
38+
{ assessmentId: umlId },
39+
{ fetchPolicy: "network-only", fetchKey: refreshKey }
40+
);
41+
42+
const studentIds = useMemo(() => {
43+
const ids =
44+
data?.getUmlExerciseByAssessmentId?.studentSubmissions?.map(
45+
(sub: any) => sub.studentId
46+
) || [];
47+
48+
// Defensive: filter out empty values and duplicates before querying user infos.
49+
return Array.from(
50+
new Set(ids.filter((id: any) => typeof id === "string" && id.length > 0))
51+
);
52+
}, [data?.getUmlExerciseByAssessmentId?.studentSubmissions]);
53+
54+
// Fetch user infos for all students
55+
const userInfosData = useLazyLoadQuery<any>(
56+
umlApiFindUserInfosQuery,
57+
{ ids: studentIds },
58+
{ fetchPolicy: "network-only" }
59+
);
60+
61+
const userInfos = useMemo(() => {
62+
const userMap: Record<string, any> = {};
63+
if (userInfosData?.findUserInfos) {
64+
userInfosData.findUserInfos.forEach((user: any) => {
65+
// Backend may return null entries when referenced users no longer exist.
66+
if (user?.id) {
67+
userMap[user.id] = user;
68+
}
69+
});
70+
}
71+
return userMap;
72+
}, [userInfosData?.findUserInfos]);
73+
74+
const [updateTutorSolution, isUpdating] = useMutation(
75+
umlApiUpdateTutorSolutionMutation
76+
);
77+
78+
const exercise = data?.getUmlExerciseByAssessmentId;
79+
const content = data?.findContentsByIds?.[0];
80+
81+
const handleUpdateTutorSolution = async (newCode: string) => {
82+
let semanticModelJson: string | null = null;
83+
const semanticModelResult = await getSemanticModel(newCode);
84+
semanticModelJson = JSON.stringify(semanticModelResult);
85+
updateTutorSolution({
86+
variables: {
87+
assessmentId: umlId,
88+
tutorSolution: {
89+
diagramCode: newCode,
90+
semanticModel: semanticModelJson,
91+
},
92+
},
93+
onCompleted: () =>
94+
setSnackbar({
95+
open: true,
96+
message: "Tutor solution updated successfully!",
97+
}),
98+
onError: () =>
99+
setSnackbar({
100+
open: true,
101+
message: "Failed to update tutor solution.",
102+
}),
103+
});
104+
};
105+
106+
if (!exercise || !content)
107+
return (
108+
<Box p={4} textAlign="center">
109+
<CircularProgress />
110+
</Box>
111+
);
112+
113+
return (
114+
<Box sx={{ width: "100%", position: "relative" }}>
115+
{/* Top Action Bar */}
116+
<Stack
117+
direction="row"
118+
justifyContent="space-between"
119+
alignItems="center"
120+
mb={2}
121+
>
122+
<Tabs
123+
value={tabIndex}
124+
onChange={(_, v) => setTabIndex(v)}
125+
color="primary"
126+
>
127+
<Tab icon={<InfoIcon />} iconPosition="start" label="Exercise Info" />
128+
<Tab icon={<PeopleIcon />} iconPosition="start" label="Submissions" />
129+
</Tabs>
130+
131+
<Button
132+
variant="outlined"
133+
startIcon={<EditIcon />}
134+
onClick={() => setIsEditModalOpen(true)}
135+
sx={{ borderRadius: 2 }}
136+
>
137+
Edit Exercise
138+
</Button>
139+
</Stack>
140+
141+
{/* Tab Content */}
142+
<Box mt={2}>
143+
{tabIndex === 0 && !isEditModalOpen ? (
144+
<ExerciseInfoTab
145+
exercise={exercise}
146+
onUpdateTutorSolution={handleUpdateTutorSolution}
147+
isUpdating={isUpdating}
148+
/>
149+
) : tabIndex === 1 ? (
150+
<SubmissionsTab exercise={exercise} userInfos={userInfos} />
151+
) : null}
152+
</Box>
153+
{isEditModalOpen && exercise && content && (
154+
<AddUMLAssignmentModal
155+
key={umlId + "-" + String(isEditModalOpen)}
156+
open={isEditModalOpen}
157+
onClose={() => setIsEditModalOpen(false)}
158+
onUpdated={() => setRefreshKey((prev) => prev + 1)}
159+
assessmentId={umlId as string}
160+
chapterId={content.metadata?.chapterId}
161+
initialData={{
162+
description: exercise.description,
163+
gradingRules: exercise.gradingRules || "",
164+
diagramCode: exercise.tutorSolution?.diagramCode,
165+
totalPoints: exercise.totalPoints,
166+
requiredPercentage: exercise.requiredPercentage,
167+
showSolution: exercise.showSolution ?? true,
168+
metadata: {
169+
name: content.metadata?.name || "",
170+
rewardPoints: content.metadata?.rewardPoints || 0,
171+
suggestedDate: content.metadata?.suggestedDate,
172+
tagNames: content.metadata?.tagNames || [],
173+
},
174+
assessmentMetadata: {
175+
skillPoints: content.assessmentMetadata?.skillPoints || 0,
176+
skillTypes: content.assessmentMetadata?.skillTypes || [],
177+
initialLearningInterval:
178+
content.assessmentMetadata?.initialLearningInterval ?? null,
179+
},
180+
}}
181+
/>
182+
)}
183+
184+
<Snackbar
185+
open={snackbar.open}
186+
autoHideDuration={4000}
187+
onClose={() => setSnackbar({ ...snackbar, open: false })}
188+
message={snackbar.message}
189+
/>
190+
</Box>
191+
);
192+
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
"use client";
2+
3+
import LecturerUmlAssignment from "@/app/courses/[courseId]/uml/[umlId]/lecturer";
4+
import StudentUMLAssignment from "@/app/courses/[courseId]/uml/[umlId]/student";
5+
import { PageError } from "@/components/PageError";
6+
import { PageView, usePageView } from "@/src/currentView";
7+
import { isUUID } from "@/src/utils";
8+
import { Container } from "@mui/material";
9+
import { useParams } from "next/navigation";
10+
11+
export default function UMLAssignmentPage() {
12+
const [pageView] = usePageView();
13+
const { umlId } = useParams();
14+
15+
if (!isUUID(umlId)) {
16+
return <PageError message="Invalid UML assessment id." />;
17+
}
18+
19+
switch (pageView) {
20+
case PageView.Student:
21+
return <StudentUMLAssignment />;
22+
23+
case PageView.Lecturer:
24+
return (
25+
<Container maxWidth={false} sx={{ py: 2 }}>
26+
<LecturerUmlAssignment />
27+
</Container>
28+
);
29+
30+
default:
31+
return <PageError message="Unauthorized or unknown view state." />;
32+
}
33+
}

0 commit comments

Comments
 (0)