From 2f8c6678824167eab0c3c60f04d0f333dfa25d77 Mon Sep 17 00:00:00 2001 From: PauloV1 Date: Sat, 15 Nov 2025 21:12:07 -0300 Subject: [PATCH 01/25] feat(self-evaluation): add attribute and methods to manage self-evaluations --- server/src/models/Enrollment.ts | 79 +++++++++++++++++++++++++-------- 1 file changed, 60 insertions(+), 19 deletions(-) diff --git a/server/src/models/Enrollment.ts b/server/src/models/Enrollment.ts index f13a0e94..4a7a5ff2 100644 --- a/server/src/models/Enrollment.ts +++ b/server/src/models/Enrollment.ts @@ -1,18 +1,20 @@ import { Student } from './Student'; -import { Evaluation } from './Evaluation'; +import { Evaluation, Grade } from './Evaluation'; export class Enrollment { private student: Student; private evaluations: Evaluation[]; + private selfEvaluations: Evaluation[]; // Média do estudante antes da prova final private mediaPreFinal: number; // Média do estudante depois da final private mediaPosFinal: number; private reprovadoPorFalta: Boolean; - constructor(student: Student, evaluations: Evaluation[] = [], mediaPreFinal: number = 0, mediaPosFinal: number = 0, reprovadoPorFalta: Boolean = false) { + constructor(student: Student, evaluations: Evaluation[] = [], selfEvaluations: Evaluation[] = [], mediaPreFinal: number = 0, mediaPosFinal: number = 0, reprovadoPorFalta: Boolean = false) { this.student = student; this.evaluations = evaluations; + this.selfEvaluations = selfEvaluations; this.mediaPreFinal = mediaPreFinal; this.mediaPosFinal = mediaPosFinal; this.reprovadoPorFalta = reprovadoPorFalta; @@ -23,11 +25,6 @@ export class Enrollment { return this.student; } - // Get evaluations - getEvaluations(): Evaluation[] { - return [...this.evaluations]; // Return copy to prevent external modification - } - // Get media do estudante antes da prova final getMediaPreFinal(): number{ return this.mediaPreFinal; @@ -58,45 +55,89 @@ export class Enrollment { this.reprovadoPorFalta = reprovadoPorFalta; } + private clone(list: Evaluation[]): Evaluation[] { + return [...list]; + } + // Add or update an evaluation - addOrUpdateEvaluation(goal: string, grade: 'MANA' | 'MPA' | 'MA'): void { - const existingIndex = this.evaluations.findIndex(evaluation => evaluation.getGoal() === goal); + private addOrUpdateIn(list: Evaluation[], goal: string, grade: Grade): void { + const existingIndex = list.findIndex(evaluation => evaluation.getGoal() === goal); if (existingIndex >= 0) { - this.evaluations[existingIndex].setGrade(grade); + list[existingIndex].setGrade(grade); } else { - this.evaluations.push(new Evaluation(goal, grade)); + list.push(new Evaluation(goal, grade)); } } // Remove an evaluation - removeEvaluation(goal: string): boolean { - const existingIndex = this.evaluations.findIndex(evaluation => evaluation.getGoal() === goal); + private removeFrom(list: Evaluation[], goal: string): boolean { + const existingIndex = list.findIndex(evaluation => evaluation.getGoal() === goal); if (existingIndex >= 0) { - this.evaluations.splice(existingIndex, 1); + list.splice(existingIndex, 1); return true; } return false; } + private findIn(list: Evaluation[], goal: string): Evaluation | undefined { + return list.find(evaluation => evaluation.getGoal() === goal); + } + + getEvaluations(): Evaluation[] { + return this.clone(this.evaluations); // Return copy to prevent external modification + } + + getSelfEvaluations(): Evaluation[] { + return this.clone(this.selfEvaluations); // Return copy to prevent external modification + } + + // Add or update an evaluation + addOrUpdateEvaluation(goal: string, grade: Grade): void { + this.addOrUpdateIn(this.evaluations, goal, grade); + } + + // Add or update a self-evaluation + addOrUpdateSelfEvaluation(goal: string, grade: Grade): void { + this.addOrUpdateIn(this.selfEvaluations, goal, grade); + } + + // Remove an evaluation + removeEvaluation(goal: string): boolean { + return this.removeFrom(this.evaluations, goal); + } + + // Remove a self-evaluation + removeSelfEvaluation(goal: string): boolean { + return this.removeFrom(this.selfEvaluations, goal); + } + // Get evaluation for a specific goal getEvaluationForGoal(goal: string): Evaluation | undefined { - return this.evaluations.find(evaluation => evaluation.getGoal() === goal); + return this.findIn(this.evaluations, goal); + } + + // Get self-evaluation for a specific goal + getSelfEvaluationForGoal(goal: string): Evaluation | undefined { + return this.findIn(this.selfEvaluations, goal); } // Convert to JSON for API responses toJSON() { return { student: this.student.toJSON(), - evaluations: this.evaluations.map(evaluation => evaluation.toJSON()) + evaluations: this.evaluations.map(evaluation => evaluation.toJSON()), + selfEvaluations: this.selfEvaluations.map(selfEvaluation => selfEvaluation.toJSON()) }; } // Create Enrollment from JSON object - static fromJSON(data: { student: any; evaluations: any[] }, student: Student): Enrollment { + static fromJSON(data: { student: any; evaluations: any[]; selfEvaluations: any[] }, student: Student): Enrollment { const evaluations = data.evaluations ? data.evaluations.map((evalData: any) => Evaluation.fromJSON(evalData)) : []; - - return new Enrollment(student, evaluations); + const selfEvaluations = data.selfEvaluations + ? data.selfEvaluations.map((evalData: any) => Evaluation.fromJSON(evalData)) + : []; + return new Enrollment(student, evaluations, selfEvaluations); } } From a8506acd8d368334620e1fc3b4fe2fda856a252a Mon Sep 17 00:00:00 2001 From: PauloV1 Date: Sat, 15 Nov 2025 22:33:33 -0300 Subject: [PATCH 02/25] feat(self-evaluation): extend updateClass to merge self-evaluations --- server/src/models/Classes.ts | 26 ++++++++++++++++---------- 1 file changed, 16 insertions(+), 10 deletions(-) diff --git a/server/src/models/Classes.ts b/server/src/models/Classes.ts index b51640ed..747bd7d8 100644 --- a/server/src/models/Classes.ts +++ b/server/src/models/Classes.ts @@ -1,4 +1,5 @@ import { Class } from './Class'; +import { Enrollment } from './Enrollment'; import { Student } from './Student'; export class Classes { @@ -31,6 +32,17 @@ export class Classes { return true; } + // Merge evaluations and self-evaluations from one enrollment to another + private mergeEnrollmentsData(from: Enrollment, to: Enrollment): void { + from.getEvaluations().forEach(evaluation => + to.addOrUpdateEvaluation(evaluation.getGoal(), evaluation.getGrade()) + ); + + from.getSelfEvaluations().forEach(selfEvaluation => + to.addOrUpdateSelfEvaluation(selfEvaluation.getGoal(), selfEvaluation.getGrade()) + ); + } + // Update class updateClass(updatedClass: Class): Class { const existingClass = this.findClassById(updatedClass.getClassId()); @@ -54,22 +66,16 @@ export class Classes { const existingEnrollment = existingClass.findEnrollmentByStudentCPF(studentCPF); if (existingEnrollment) { - // Update existing enrollment's evaluations - const updatedEvaluations = updatedEnrollment.getEvaluations(); - updatedEvaluations.forEach(evaluation => { - existingEnrollment.addOrUpdateEvaluation(evaluation.getGoal(), evaluation.getGrade()); - }); + // Update existing enrollment's evaluations and self-evaluations + this.mergeEnrollmentsData(updatedEnrollment, existingEnrollment); } else { // Add new enrollment that doesn't exist yet try { existingClass.addEnrollment(updatedEnrollment.getStudent()); const newEnrollment = existingClass.findEnrollmentByStudentCPF(studentCPF); if (newEnrollment) { - // Copy over evaluations from updated enrollment - const updatedEvaluations = updatedEnrollment.getEvaluations(); - updatedEvaluations.forEach(evaluation => { - newEnrollment.addOrUpdateEvaluation(evaluation.getGoal(), evaluation.getGrade()); - }); + // Copy over evaluations and self-evaluations from updated enrollment + this.mergeEnrollmentsData(updatedEnrollment, newEnrollment); } } catch (error) { // Enrollment already exists, this shouldn't happen but handle gracefully From 68aee91ac1252acc0acc8b869e2ed935741d9936 Mon Sep 17 00:00:00 2001 From: PauloV1 Date: Sat, 15 Nov 2025 22:40:26 -0300 Subject: [PATCH 03/25] feat(self-evaluation): add methods to merge evaluations and self-evaluations from another Enrollment --- server/src/models/Enrollment.ts | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/server/src/models/Enrollment.ts b/server/src/models/Enrollment.ts index 4a7a5ff2..e16ee9e4 100644 --- a/server/src/models/Enrollment.ts +++ b/server/src/models/Enrollment.ts @@ -121,6 +121,20 @@ export class Enrollment { return this.findIn(this.selfEvaluations, goal); } + // Merge evaluations from another enrollment + mergeEvaluationsFrom(other: Enrollment): void { + other.getEvaluations().forEach(evaluation => { + this.addOrUpdateEvaluation(evaluation.getGoal(), evaluation.getGrade()); + }); + } + + // Merge self-evaluations from another enrollment + mergeSelfEvaluationsFrom(other: Enrollment): void { + other.getSelfEvaluations().forEach(selfEvaluation => { + this.addOrUpdateSelfEvaluation(selfEvaluation.getGoal(), selfEvaluation.getGrade()); + }); + } + // Convert to JSON for API responses toJSON() { return { From 2c1dae6e463f87ff38d9b2d6831df2f2509dea1e Mon Sep 17 00:00:00 2001 From: PauloV1 Date: Sat, 15 Nov 2025 22:49:09 -0300 Subject: [PATCH 04/25] refactor(enrollment): move evaluations merge logic into Enrollment and update Class to use it --- server/src/models/Classes.ts | 17 ++++------------- 1 file changed, 4 insertions(+), 13 deletions(-) diff --git a/server/src/models/Classes.ts b/server/src/models/Classes.ts index 747bd7d8..1eaf60a8 100644 --- a/server/src/models/Classes.ts +++ b/server/src/models/Classes.ts @@ -32,17 +32,6 @@ export class Classes { return true; } - // Merge evaluations and self-evaluations from one enrollment to another - private mergeEnrollmentsData(from: Enrollment, to: Enrollment): void { - from.getEvaluations().forEach(evaluation => - to.addOrUpdateEvaluation(evaluation.getGoal(), evaluation.getGrade()) - ); - - from.getSelfEvaluations().forEach(selfEvaluation => - to.addOrUpdateSelfEvaluation(selfEvaluation.getGoal(), selfEvaluation.getGrade()) - ); - } - // Update class updateClass(updatedClass: Class): Class { const existingClass = this.findClassById(updatedClass.getClassId()); @@ -67,7 +56,8 @@ export class Classes { if (existingEnrollment) { // Update existing enrollment's evaluations and self-evaluations - this.mergeEnrollmentsData(updatedEnrollment, existingEnrollment); + existingEnrollment.mergeEvaluationsFrom(updatedEnrollment); + existingEnrollment.mergeSelfEvaluationsFrom(updatedEnrollment); } else { // Add new enrollment that doesn't exist yet try { @@ -75,7 +65,8 @@ export class Classes { const newEnrollment = existingClass.findEnrollmentByStudentCPF(studentCPF); if (newEnrollment) { // Copy over evaluations and self-evaluations from updated enrollment - this.mergeEnrollmentsData(updatedEnrollment, newEnrollment); + newEnrollment.mergeEvaluationsFrom(updatedEnrollment); + newEnrollment.mergeSelfEvaluationsFrom(updatedEnrollment); } } catch (error) { // Enrollment already exists, this shouldn't happen but handle gracefully From afdf804f896bbd625ada7d2954134944b22a6735 Mon Sep 17 00:00:00 2001 From: PauloV1 Date: Sat, 15 Nov 2025 23:58:10 -0300 Subject: [PATCH 05/25] feat(self-evaluation): load selfEvaluations from file and add PUT endpoint --- server/src/server.ts | 116 +++++++++++++++++++++++++++---------------- 1 file changed, 74 insertions(+), 42 deletions(-) diff --git a/server/src/server.ts b/server/src/server.ts index 6e2597b3..61b6e761 100644 --- a/server/src/server.ts +++ b/server/src/server.ts @@ -2,7 +2,7 @@ import express, { Request, Response } from 'express'; import cors from 'cors'; import { StudentSet } from './models/StudentSet'; import { Student } from './models/Student'; -import { Evaluation } from './models/Evaluation'; +import { Evaluation, Grade } from './models/Evaluation'; import { Classes } from './models/Classes'; import { Class } from './models/Class'; import * as fs from 'fs'; @@ -48,7 +48,8 @@ const saveDataToFile = (): void => { year: classObj.getYear(), enrollments: classObj.getEnrollments().map(enrollment => ({ studentCPF: enrollment.getStudent().getCPF(), - evaluations: enrollment.getEvaluations().map(evaluation => evaluation.toJSON()) + evaluations: enrollment.getEvaluations().map(evaluation => evaluation.toJSON()), + selfEvaluations: enrollment.getSelfEvaluations().map(selfEvaluation => selfEvaluation.toJSON()) })) })) }; @@ -60,6 +61,16 @@ const saveDataToFile = (): void => { } }; +const loadEvaluations = ( + evalArray: any[], + addFn: (goal: string, grade: Grade) => void +) => { + evalArray.forEach((e: any) => { + const evaluation = Evaluation.fromJSON(e); + addFn(evaluation.getGoal(), evaluation.getGrade()); + }); +}; + // Load data from file const loadDataFromFile = (): void => { try { @@ -99,13 +110,16 @@ const loadDataFromFile = (): void => { if (student) { const enrollment = classObj.addEnrollment(student); - // Load evaluations for this enrollment + // Load evaluations if (enrollmentData.evaluations && Array.isArray(enrollmentData.evaluations)) { - enrollmentData.evaluations.forEach((evalData: any) => { - const evaluation = Evaluation.fromJSON(evalData); - enrollment.addOrUpdateEvaluation(evaluation.getGoal(), evaluation.getGrade()); - }); + loadEvaluations(enrollmentData.evaluations, enrollment.addOrUpdateEvaluation.bind(enrollment)); + } + + // Load self-evaluations + if (enrollmentData.selfEvaluations && Array.isArray(enrollmentData.selfEvaluations)) { + loadEvaluations(enrollmentData.selfEvaluations, enrollment.addOrUpdateSelfEvaluation.bind(enrollment)); } + } else { console.error(`Student with CPF ${enrollmentData.studentCPF} not found for enrollment`); } @@ -137,6 +151,53 @@ const cleanCPF = (cpf: string): string => { return cpf.replace(/[.-]/g, ''); }; +// Handlers for evaluation and self-evaluation updates +const handleEvaluationUpdate = (req: Request, res: Response, options: { + type: 'evaluation' | 'selfEvaluation'; +}) => { + try { + const { classId, studentCPF } = req.params; + const { goal, grade } = req.body; + + if (!goal) { + return res.status(400).json({ error: 'Goal is required' }); + } + + const classObj = classes.findClassById(classId); + if (!classObj) { + return res.status(404).json({ error: 'Class not found' }); + } + + const cleanedCPF = cleanCPF(studentCPF); + const enrollment = classObj.findEnrollmentByStudentCPF(cleanedCPF); + if (!enrollment) { + return res.status(404).json({ error: 'Student not enrolled in this class' }); + } + + const isSelf = options.type === 'selfEvaluation'; + + if (grade === '' || grade === null || grade === undefined) { + isSelf + ? enrollment.removeSelfEvaluation(goal) + : enrollment.removeEvaluation(goal); + } else { + if (!['MANA', 'MPA', 'MA'].includes(grade)) { + return res.status(400).json({ error: 'Invalid grade. Must be MANA, MPA or MA' }); + } + + isSelf + ? enrollment.addOrUpdateSelfEvaluation(goal, grade) + : enrollment.addOrUpdateEvaluation(goal, grade); + } + + triggerSave(); + res.json(enrollment.toJSON()); + + } catch (error) { + res.status(400).json({ error: (error as Error).message }); + } +} + // Routes // GET /api/students - Get all students @@ -403,43 +464,14 @@ app.get('/api/classes/:classId/enrollments', (req: Request, res: Response) => { }); // PUT /api/classes/:classId/enrollments/:studentCPF/evaluation - Update evaluation for an enrolled student -app.put('/api/classes/:classId/enrollments/:studentCPF/evaluation', (req: Request, res: Response) => { - try { - const { classId, studentCPF } = req.params; - const { goal, grade } = req.body; - - if (!goal) { - return res.status(400).json({ error: 'Goal is required' }); - } +app.put('/api/classes/:classId/enrollments/:studentCPF/evaluation', (req, res) => + handleEvaluationUpdate(req, res, { type: 'evaluation' }) +); - const classObj = classes.findClassById(classId); - if (!classObj) { - return res.status(404).json({ error: 'Class not found' }); - } +app.put('/api/classes/:classId/enrollments/:studentCPF/selfEvaluation', (req, res) => + handleEvaluationUpdate(req, res, { type: 'selfEvaluation' }) +); - const cleanedCPF = cleanCPF(studentCPF); - const enrollment = classObj.findEnrollmentByStudentCPF(cleanedCPF); - if (!enrollment) { - return res.status(404).json({ error: 'Student not enrolled in this class' }); - } - - if (grade === '' || grade === null || grade === undefined) { - // Remove evaluation - enrollment.removeEvaluation(goal); - } else { - // Add or update evaluation - if (!['MANA', 'MPA', 'MA'].includes(grade)) { - return res.status(400).json({ error: 'Invalid grade. Must be MANA, MPA, or MA' }); - } - enrollment.addOrUpdateEvaluation(goal, grade); - } - - triggerSave(); // Save to file after evaluation update - res.json(enrollment.toJSON()); - } catch (error) { - res.status(400).json({ error: (error as Error).message }); - } -}); // POST api/classes/gradeImport/:classId, usado na feature de importacao de grades // Vai ser usado em 2 fluxos(poderia ter divido em 2 endpoints mas preferi deixar em apenas 1) From 25d080aa453f5f9c58bb2c4456b71c5e58f1fc1d Mon Sep 17 00:00:00 2001 From: PauloV1 Date: Sun, 30 Nov 2025 11:02:12 -0300 Subject: [PATCH 06/25] feat(self-evaluation): add frontend for self-evaluation feature --- client/package-lock.json | 23 ++ client/src/App.css | 219 +++++++++++++ client/src/App.tsx | 71 +++- client/src/components/Evaluations.tsx | 375 ++++++++++++++++++--- client/src/components/SelfEvaluation.tsx | 395 +++++++++++++++++++++++ client/src/services/EnrollmentService.ts | 22 ++ client/src/types/Enrollment.ts | 3 + package-lock.json | 25 ++ server/data/app-data.json | 115 ++++++- server/package-lock.json | 2 + 10 files changed, 1197 insertions(+), 53 deletions(-) create mode 100644 client/src/components/SelfEvaluation.tsx diff --git a/client/package-lock.json b/client/package-lock.json index 8a2ba506..8d851e86 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -58,6 +58,7 @@ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.5.tgz", "integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==", "license": "MIT", + "peer": true, "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.5", @@ -707,6 +708,7 @@ "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-flow/-/plugin-syntax-flow-7.27.1.tgz", "integrity": "sha512-p9OkPbZ5G7UT1MofwYFigGebnrzGJacoBSQM0/6bi/PUMVE+qlWDD/OalvQKbwgQzU6dl0xAv6r4X7Jme0RYxA==", "license": "MIT", + "peer": true, "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, @@ -1590,6 +1592,7 @@ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx/-/plugin-transform-react-jsx-7.27.1.tgz", "integrity": "sha512-2KH4LWGSrJIkVf5tSiBFYuXDAoWRq2MMwgivCf+93dd0GQi8RXLjKA/0EvRnVV5G0hrHczsquXuD01L8s6dmBw==", "license": "MIT", + "peer": true, "dependencies": { "@babel/helper-annotate-as-pure": "^7.27.1", "@babel/helper-module-imports": "^7.27.1", @@ -3706,6 +3709,7 @@ "integrity": "sha512-RFA/bURkcKzx/X9oumPG9Vp3D3JUgus/d0b67KB0t5S/raciymilkOa66olh78MUI92QLbEJevO7rvqU/kjwKA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@types/prop-types": "*", "csstype": "^3.0.2" @@ -3831,6 +3835,7 @@ "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.62.0.tgz", "integrity": "sha512-TiZzBSJja/LbhNPvk6yc0JrX9XqhQ0hdh6M2svYfsHGejaKFIAGd9MQ+ERIMzLGlN/kZoYIgdxFV0PuljTKXag==", "license": "MIT", + "peer": true, "dependencies": { "@eslint-community/regexpp": "^4.4.0", "@typescript-eslint/scope-manager": "5.62.0", @@ -3884,6 +3889,7 @@ "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.62.0.tgz", "integrity": "sha512-VlJEV0fOQ7BExOsHYAGrgbEiZoi8D+Bl2+f6V2RrXerRSylnp+ZBHmPvaIa8cz0Ajx7WO7Z5RqfgYg7ED1nRhA==", "license": "BSD-2-Clause", + "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "5.62.0", "@typescript-eslint/types": "5.62.0", @@ -4253,6 +4259,7 @@ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -4351,6 +4358,7 @@ "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", "license": "MIT", + "peer": true, "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", @@ -5261,6 +5269,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "baseline-browser-mapping": "^2.8.19", "caniuse-lite": "^1.0.30001751", @@ -7086,6 +7095,7 @@ "integrity": "sha512-ypowyDxpVSYpkXr9WPv2PAZCtNip1Mv5KTW0SCurXv/9iOpcrH9PaqUElksqEB6pChqHGDRCFTyrZlGhnLNGiA==", "deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.", "license": "MIT", + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", @@ -9853,6 +9863,7 @@ "resolved": "https://registry.npmjs.org/jest/-/jest-27.5.1.tgz", "integrity": "sha512-Yn0mADZB89zTtjkPJEXwrac3LHudkQMR+Paqa8uxJHCBr9agxztUifWCyiYrjhMPBoUVBjyny0I7XH6ozDr7QQ==", "license": "MIT", + "peer": true, "dependencies": { "@jest/core": "^27.5.1", "import-local": "^3.0.2", @@ -10750,6 +10761,7 @@ "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz", "integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==", "license": "MIT", + "peer": true, "bin": { "jiti": "bin/jiti.js" } @@ -12115,6 +12127,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", @@ -13249,6 +13262,7 @@ "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz", "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==", "license": "MIT", + "peer": true, "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" @@ -13608,6 +13622,7 @@ "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", "license": "MIT", + "peer": true, "dependencies": { "loose-envify": "^1.1.0" }, @@ -13767,6 +13782,7 @@ "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.11.0.tgz", "integrity": "sha512-F27qZr8uUqwhWZboondsPx8tnC3Ct3SxZA3V5WyEvujRyyNv0VYPhoBg1gZ8/MV5tubQp76Trw8lTv9hzRBa+A==", "license": "MIT", + "peer": true, "engines": { "node": ">=0.10.0" } @@ -14200,6 +14216,7 @@ "resolved": "https://registry.npmjs.org/rollup/-/rollup-2.79.2.tgz", "integrity": "sha512-fS6iqSPZDs3dr/y7Od6y5nha8dW1YnbgtsyotCVvoFGKbERG++CVRFv1meyGDE1SNItQA8BrnCw7ScdAhRJ3XQ==", "license": "MIT", + "peer": true, "bin": { "rollup": "dist/bin/rollup" }, @@ -14445,6 +14462,7 @@ "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", "license": "MIT", + "peer": true, "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", @@ -16106,6 +16124,7 @@ "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", "license": "(MIT OR CC0-1.0)", + "peer": true, "engines": { "node": ">=10" }, @@ -16214,6 +16233,7 @@ "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz", "integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==", "license": "Apache-2.0", + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -16528,6 +16548,7 @@ "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.102.1.tgz", "integrity": "sha512-7h/weGm9d/ywQ6qzJ+Xy+r9n/3qgp/thalBbpOi5i223dPXKi04IBtqPN9nTd+jBc7QKfvDbaBnFipYp4sJAUQ==", "license": "MIT", + "peer": true, "dependencies": { "@types/eslint-scope": "^3.7.7", "@types/estree": "^1.0.8", @@ -16599,6 +16620,7 @@ "resolved": "https://registry.npmjs.org/webpack-dev-server/-/webpack-dev-server-4.15.2.tgz", "integrity": "sha512-0XavAZbNJ5sDrCbkpWL8mia0o5WPOd2YGtxrEiZkBK9FjLppIUK2TgxK6qGD2P3hUXTJNNPVibrerKcx5WkR1g==", "license": "MIT", + "peer": true, "dependencies": { "@types/bonjour": "^3.5.9", "@types/connect-history-api-fallback": "^1.3.5", @@ -17011,6 +17033,7 @@ "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", "license": "MIT", + "peer": true, "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", diff --git a/client/src/App.css b/client/src/App.css index 8cabd26f..355a51a7 100644 --- a/client/src/App.css +++ b/client/src/App.css @@ -1192,4 +1192,223 @@ tbody tr:last-child td { .evaluation-select option[value="MANA"] { background-color: #fecaca; color: #991b1b; +} + +/* Self-Evaluation specific styles */ +.self-evaluation-section { + width: 100%; +} + +.self-evaluation-section h3 { + color: #1f2937; + margin-bottom: 1.5rem; + font-size: 1.75rem; + font-weight: 700; +} + +.search-container input[type="email"], +.search-container input[type="text"] { + transition: all 0.2s ease; +} + +.search-container input[type="email"]:disabled, +.search-container input[type="text"]:disabled { + background-color: #f3f4f6; + cursor: not-allowed; + opacity: 0.6; +} + +/* Grade color variables for comparison view */ +:root { + --grade-ma-bg: linear-gradient(135deg, #10b981 0%, #047857 100%); + --grade-ma-color: white; + --grade-mpa-bg: linear-gradient(135deg, #f59e0b 0%, #d97706 100%); + --grade-mpa-color: white; + --grade-mana-bg: linear-gradient(135deg, #ef4444 0%, #dc2626 100%); + --grade-mana-color: white; +} + +/* Comparison view grade styling */ +.evaluation-section span[style*="grade-ma"] { + background: linear-gradient(135deg, #10b981 0%, #047857 100%) !important; + color: white !important; +} + +.evaluation-section span[style*="grade-mpa"] { + background: linear-gradient(135deg, #f59e0b 0%, #d97706 100%) !important; + color: white !important; +} + +.evaluation-section span[style*="grade-mana"] { + background: linear-gradient(135deg, #ef4444 0%, #dc2626 100%) !important; + color: white !important; +} + +.self-evaluation-matrix { + overflow-x: auto; + border-radius: 12px; + box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1), 0 1px 3px rgba(0, 0, 0, 0.08); + border: 1px solid #cbd5e1; +} + +.self-evaluation-matrix .evaluation-table { + width: 100%; + border-collapse: collapse; + background: white; + min-width: 600px; +} + +.self-evaluation-matrix .evaluation-table thead tr { + background: linear-gradient(135deg, #059669 0%, #047857 100%); +} + +.self-evaluation-matrix .evaluation-table th { + background: linear-gradient(135deg, #059669 0%, #047857 100%); + color: white; + padding: 16px; + text-align: center; + font-weight: 700; + border: none; + font-size: 1rem; + text-shadow: 0 1px 2px rgba(0,0,0,0.3); + box-shadow: inset 0 1px 0 rgba(255,255,255,0.2); +} + +.self-evaluation-matrix .evaluation-table th:first-child { + text-align: left; + width: 50%; +} + +.self-evaluation-matrix .evaluation-table th:last-child { + text-align: center; + width: 50%; +} + +.self-evaluation-matrix .evaluation-table tbody tr { + border-bottom: 1px solid #cbd5e1; + transition: background-color 0.2s ease; +} + +.self-evaluation-matrix .evaluation-table tbody tr:nth-child(even) { + background-color: #f0f9ff; +} + +.self-evaluation-matrix .evaluation-table tbody tr:nth-child(odd) { + background-color: #ffffff; +} + +.self-evaluation-matrix .evaluation-table tbody tr:hover { + background-color: #e0f2fe; + transform: scale(1.001); +} + +.self-evaluation-matrix .evaluation-table td { + padding: 12px 8px; + text-align: center; + border: 1px solid #cbd5e1; + vertical-align: middle; +} + +.self-evaluation-matrix .evaluation-table td:first-child { + text-align: left; + color: #0c4a6e; + font-weight: 600; + background: linear-gradient(135deg, #e0f2fe 0%, #bae6fd 100%); + position: relative; + z-index: 1; + padding: 16px; +} + +.self-evaluation-matrix .evaluation-table tbody tr:nth-child(even) td:first-child { + background: linear-gradient(135deg, #bae6fd 0%, #7dd3fc 100%); +} + +.self-evaluation-matrix .evaluation-table tbody tr:hover td:first-child { + background: linear-gradient(135deg, #7dd3fc 0%, #38bdf8 100%); +} + +/* Style for grade spans in self-evaluation table */ +.self-evaluation-matrix .evaluation-table td:not(:first-child) span { + display: inline-block; + padding: 6px 10px; + border-radius: 4px; + font-weight: 600; + font-size: 0.85rem; +} + +.self-evaluation-matrix .evaluation-table td:not(:first-child) span[style*="background"] { + text-shadow: 0 1px 2px rgba(0,0,0,0.3); +} + +.self-evaluation-matrix .evaluation-select { + width: 100%; + max-width: 120px; + padding: 10px 12px; + border: 2px solid #e5e7eb; + border-radius: 6px; + font-size: 0.95rem; + font-weight: 600; + background-color: white; + cursor: pointer; + transition: all 0.3s ease; + text-align: center; + text-align-last: center; +} + +.self-evaluation-matrix .evaluation-select:hover { + border-color: #3b82f6; + transform: scale(1.05); + box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); +} + +.self-evaluation-matrix .evaluation-select:focus { + outline: none; + border-color: #2563eb; + box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.2); + transform: scale(1.05); +} + +/* Grade-specific colors for self-evaluation */ +.self-evaluation-matrix .evaluation-select.grade-ma { + background: linear-gradient(135deg, #10b981 0%, #047857 100%); + color: white; + border-color: #059669; + text-shadow: 0 1px 2px rgba(0,0,0,0.3); +} + +.self-evaluation-matrix .evaluation-select.grade-mpa { + background: linear-gradient(135deg, #f59e0b 0%, #d97706 100%); + color: white; + border-color: #f59e0b; + text-shadow: 0 1px 2px rgba(0,0,0,0.3); +} + +.self-evaluation-matrix .evaluation-select.grade-mana { + background: linear-gradient(135deg, #ef4444 0%, #dc2626 100%); + color: white; + border-color: #ef4444; + text-shadow: 0 1px 2px rgba(0,0,0,0.3); +} + +.self-evaluation-matrix .evaluation-select option { + background-color: white; + color: #374151; + font-weight: 600; + text-align: center; + padding: 8px; +} + +.self-evaluation-matrix .evaluation-select option[value="MA"] { + background-color: #d1fae5; + color: #065f46; +} + +.self-evaluation-matrix .evaluation-select option[value="MPA"] { + background-color: #fef3c7; + color: #92400e; +} + +.self-evaluation-matrix .evaluation-select option[value="MANA"] { + background-color: #fecaca; + color: #991b1b; } \ No newline at end of file diff --git a/client/src/App.tsx b/client/src/App.tsx index 86a0962e..c688093a 100644 --- a/client/src/App.tsx +++ b/client/src/App.tsx @@ -7,9 +7,10 @@ import StudentList from './components/StudentList'; import StudentForm from './components/StudentForm'; import Evaluations from './components/Evaluations'; import Classes from './components/Classes'; +import SelfEvaluation from './components/SelfEvaluation'; import './App.css'; -type TabType = 'students' | 'evaluations' | 'classes'; +type TabType = 'students' | 'evaluations' | 'classes' | 'self-evaluation'; const App: React.FC = () => { const [students, setStudents] = useState([]); @@ -19,6 +20,7 @@ const App: React.FC = () => { const [error, setError] = useState(''); const [editingStudent, setEditingStudent] = useState(null); const [activeTab, setActiveTab] = useState('students'); + const errorTimeoutRef = React.useRef(null); const loadStudents = useCallback(async () => { try { @@ -67,6 +69,40 @@ const App: React.FC = () => { loadClasses(); }, [loadStudents, loadClasses]); + // Clear error message when window loses focus or after 6 seconds + useEffect(() => { + const handleWindowBlur = () => { + setError(''); + if (errorTimeoutRef.current) { + clearTimeout(errorTimeoutRef.current); + } + }; + + window.addEventListener('blur', handleWindowBlur); + return () => { + window.removeEventListener('blur', handleWindowBlur); + }; + }, []); + + // Auto-clear error message after 6 seconds + useEffect(() => { + if (error) { + if (errorTimeoutRef.current) { + clearTimeout(errorTimeoutRef.current); + } + + errorTimeoutRef.current = setTimeout(() => { + setError(''); + }, 6000); + } + + return () => { + if (errorTimeoutRef.current) { + clearTimeout(errorTimeoutRef.current); + } + }; + }, [error]); + const handleStudentAdded = async () => { loadStudents(); // Reload the list when a new student is added const updatedClasses = await loadClasses(); // Also reload classes to update enrollment info @@ -120,6 +156,27 @@ const App: React.FC = () => { {error && (
Error: {error} +
)} @@ -137,6 +194,12 @@ const App: React.FC = () => { > Evaluations + + + + + {/* Evaluations View */} + {viewMode === 'evaluations' && ( +
+ + + + + {evaluationGoals.map(goal => ( + + ))} + + + + {selectedClass.enrollments.map(enrollment => { + const student = enrollment.student; + + // Create a map of evaluations for quick lookup + const studentEvaluations = enrollment.evaluations.reduce((acc, evaluation) => { + acc[evaluation.goal] = evaluation.grade; + return acc; + }, {} as {[goal: string]: string}); + + return ( + + + {evaluationGoals.map(goal => { + const currentGrade = studentEvaluations[goal] || ''; + + return ( + + ); + })} + + ); + })} + +
Student{goal}
{student.name} + +
+
+ )} + + {/* Self-Evaluations View */} + {viewMode === 'self-evaluations' && ( +
+ + + + + {evaluationGoals.map(goal => ( + + ))} + + + + {selectedClass.enrollments.map(enrollment => { + const student = enrollment.student; + + // Create a map of self-evaluations for quick lookup + const studentSelfEvaluations = enrollment.selfEvaluations.reduce((acc, evaluation) => { + acc[evaluation.goal] = evaluation.grade; + return acc; + }, {} as {[goal: string]: string}); + + return ( + + + {evaluationGoals.map(goal => { + const currentGrade = studentSelfEvaluations[goal] || ''; + + const getGradeStyle = (grade: string) => { + switch(grade) { + case 'MA': + return { + background: 'linear-gradient(135deg, #10b981 0%, #047857 100%)', + color: 'white', + fontWeight: '600', + textShadow: '0 1px 2px rgba(0,0,0,0.3)' + }; + case 'MPA': + return { + background: 'linear-gradient(135deg, #f59e0b 0%, #d97706 100%)', + color: 'white', + fontWeight: '600', + textShadow: '0 1px 2px rgba(0,0,0,0.3)' + }; + case 'MANA': + return { + background: 'linear-gradient(135deg, #ef4444 0%, #dc2626 100%)', + color: 'white', + fontWeight: '600', + textShadow: '0 1px 2px rgba(0,0,0,0.3)' + }; + default: + return { + backgroundColor: 'transparent', + color: '#9ca3af' + }; + } + }; + + return ( + + ); + })} + + ); + })} + +
Student{goal}
{student.name} + + {currentGrade || '-'} + +
+
+ )} + + {/* Comparison View */} + {viewMode === 'comparison' && ( +
+ + + + + {evaluationGoals.map(goal => ( + + ))} + + + + {evaluationGoals.map(goal => ( + + + + + ))} + + + + {selectedClass.enrollments.map(enrollment => { + const student = enrollment.student; + + const studentEvaluations = enrollment.evaluations.reduce((acc, evaluation) => { + acc[evaluation.goal] = evaluation.grade; + return acc; + }, {} as {[goal: string]: string}); + + const studentSelfEvaluations = enrollment.selfEvaluations.reduce((acc, evaluation) => { + acc[evaluation.goal] = evaluation.grade; + return acc; + }, {} as {[goal: string]: string}); + + return ( + + + {evaluationGoals.map(goal => { + const evaluation = studentEvaluations[goal] || ''; + const selfEvaluation = studentSelfEvaluations[goal] || ''; + const discrepancyClass = getDiscrepancyClass(evaluation, selfEvaluation); + + const getGradeStyle = (grade: string) => { + switch(grade) { + case 'MA': + return { + background: 'linear-gradient(135deg, #10b981 0%, #047857 100%)', + color: 'white', + fontWeight: '600', + textShadow: '0 1px 2px rgba(0,0,0,0.3)' + }; + case 'MPA': + return { + background: 'linear-gradient(135deg, #f59e0b 0%, #d97706 100%)', + color: 'white', + fontWeight: '600', + textShadow: '0 1px 2px rgba(0,0,0,0.3)' + }; + case 'MANA': + return { + background: 'linear-gradient(135deg, #ef4444 0%, #dc2626 100%)', + color: 'white', + fontWeight: '600', + textShadow: '0 1px 2px rgba(0,0,0,0.3)' + }; + default: + return { + backgroundColor: 'transparent', + color: '#9ca3af' + }; + } + }; + + return ( + + + + + ); + })} + + ); + })} + +
Student + {goal} +
+ Prof + + Self +
+ {student.name} + + + {evaluation || '-'} + + + + {selfEvaluation || '-'} + +
+
+ )} )} diff --git a/client/src/components/SelfEvaluation.tsx b/client/src/components/SelfEvaluation.tsx new file mode 100644 index 00000000..c0e87680 --- /dev/null +++ b/client/src/components/SelfEvaluation.tsx @@ -0,0 +1,395 @@ +import React, { useState, useEffect, useCallback } from 'react'; +import { Class } from '../types/Class'; +import { Student } from '../types/Student'; +import ClassService from '../services/ClassService'; +import { studentService } from '../services/StudentService'; +import EnrollmentService from '../services/EnrollmentService'; + +interface SelfEvaluationProps { + onError: (errorMessage: string) => void; +} + +const SelfEvaluation: React.FC = ({ onError }) => { + const [email, setEmail] = useState(''); + const [cpf, setCpf] = useState(''); + const [classes, setClasses] = useState([]); + const [selectedClassId, setSelectedClassId] = useState(''); + const [selectedClass, setSelectedClass] = useState(null); + const [studentData, setStudentData] = useState(null); + const [isLoading, setIsLoading] = useState(false); + const [isSearching, setIsSearching] = useState(false); + + // Predefined evaluation goals + const evaluationGoals = [ + 'Requirements', + 'Configuration Management', + 'Project Management', + 'Design', + 'Tests', + 'Refactoring' + ]; + + const loadClasses = useCallback(async () => { + try { + const classesData = await ClassService.getAllClasses(); + setClasses(classesData); + } catch (error) { + onError(`Failed to load classes. Please refresh the page and try again.`); + } + }, [onError]); + + // Load all classes on component mount + useEffect(() => { + loadClasses(); + }, [loadClasses]); + + // Update selected class when selectedClassId changes + useEffect(() => { + if (selectedClassId) { + const classObj = classes.find(c => c.id === selectedClassId); + setSelectedClass(classObj || null); + } else { + setSelectedClass(null); + } + }, [selectedClassId, classes]); + + // Helper function to clean CPF + const cleanCPF = (cpf: string): string => { + return cpf.replace(/[.-]/g, ''); + }; + + const handleSearchStudent = async () => { + if (!email.trim() || !cpf.trim()) { + onError('Please enter both email and CPF to continue'); + return; + } + + try { + setIsSearching(true); + setStudentData(null); + setSelectedClassId(''); + setSelectedClass(null); + + const cleanedCPF = cleanCPF(cpf); + const student = await studentService.getStudentByCPF(cleanedCPF); + + if (student.email !== email.trim()) { + onError('The email provided does not match the CPF registered in the system. Please verify your information and try again.'); + setStudentData(null); + return; + } + + setStudentData(student); + + // Load fresh classes to get current enrollments + const classesData = await ClassService.getAllClasses(); + setClasses(classesData); + } catch (error) { + onError(`Student not found. Please verify the CPF entered (${cpf}) and try again.`); + setStudentData(null); + } finally { + setIsSearching(false); + } + }; + + // Get classes where the student is enrolled + const getEnrolledClasses = (): Class[] => { + if (!studentData) return []; + + const cleanedCPF = cleanCPF(studentData.cpf); + return classes.filter(classObj => + classObj.enrollments.some(enrollment => { + const enrollmentCPF = cleanCPF(enrollment.student.cpf); + return enrollmentCPF === cleanedCPF; + }) + ); + }; + + const handleSelfEvaluationChange = async (goal: string, grade: string) => { + if (!selectedClass || !studentData) { + onError('Please select a class before saving your evaluation'); + return; + } + + try { + await EnrollmentService.updateSelfEvaluation(selectedClass.id, studentData.cpf, goal, grade); + // Reload classes to get updated enrollment data + await loadClasses(); + } catch (error) { + const errorMessage = (error as Error).message; + if (errorMessage.includes('Invalid grade')) { + onError('Invalid grade selection. Please choose a valid option (MANA, MPA, or MA).'); + } else if (errorMessage.includes('not enrolled')) { + onError('You are not enrolled in this class. Please select a different class.'); + } else if (errorMessage.includes('not found')) { + onError('Class or student not found. Please try refreshing the page.'); + } else { + onError(`Failed to save your evaluation. Please try again. Details: ${errorMessage}`); + } + } + }; + + const enrolledClasses = getEnrolledClasses(); + + return ( +
+

Self-Evaluation

+ + {/* Student Search Section */} +
+
+
+ + setEmail(e.target.value)} + style={{ + width: '100%', + padding: '0.75rem', + borderRadius: '6px', + border: '2px solid #0ea5e9', + boxSizing: 'border-box', + fontSize: '1rem', + fontWeight: '500', + backgroundColor: 'white', + transition: 'all 0.2s ease', + color: '#0c4a6e' + }} + disabled={isSearching} + onFocus={(e) => { + e.target.style.borderColor = '#0284c7'; + e.target.style.boxShadow = '0 0 0 3px rgba(14, 165, 233, 0.2)'; + }} + onBlur={(e) => { + e.target.style.borderColor = '#0ea5e9'; + e.target.style.boxShadow = 'none'; + }} + /> +
+ +
+ + setCpf(e.target.value)} + style={{ + width: '100%', + padding: '0.75rem', + borderRadius: '6px', + border: '2px solid #0ea5e9', + boxSizing: 'border-box', + fontSize: '1rem', + fontWeight: '500', + backgroundColor: 'white', + transition: 'all 0.2s ease', + color: '#0c4a6e' + }} + disabled={isSearching} + onFocus={(e) => { + e.target.style.borderColor = '#0284c7'; + e.target.style.boxShadow = '0 0 0 3px rgba(14, 165, 233, 0.2)'; + }} + onBlur={(e) => { + e.target.style.borderColor = '#0ea5e9'; + e.target.style.boxShadow = 'none'; + }} + /> +
+ +
+ +
+
+
+ + {/* Class Selection Section */} + {studentData && enrolledClasses.length > 0 && ( +
+ + +
+ )} + + {studentData && enrolledClasses.length === 0 && ( +
+

No Classes Available

+

You are not enrolled in any classes. Please contact your instructor or administrator to register.

+
+ )} + + {selectedClass && studentData && ( +
+

+ {selectedClass.topic} ({selectedClass.year}/{selectedClass.semester}) +

+ +
+ + + + + + + + + {evaluationGoals.map((goal, index) => { + // Find the enrollment for this student in the selected class + const cleanedStudentCPF = cleanCPF(studentData.cpf); + const enrollment = selectedClass.enrollments.find( + e => cleanCPF(e.student.cpf) === cleanedStudentCPF + ); + + // Get the self-evaluation for this goal + const selfEvaluation = enrollment?.selfEvaluations.find( + se => se.goal === goal + ); + const currentGrade = selfEvaluation?.grade || ''; + + const getGradeStyle = (grade: string) => { + switch(grade) { + case 'MA': + return { + background: 'linear-gradient(135deg, #10b981 0%, #047857 100%)', + color: 'white' + }; + case 'MPA': + return { + background: 'linear-gradient(135deg, #f59e0b 0%, #d97706 100%)', + color: 'white' + }; + case 'MANA': + return { + background: 'linear-gradient(135deg, #ef4444 0%, #dc2626 100%)', + color: 'white' + }; + default: + return { + backgroundColor: 'transparent', + color: '#9ca3af' + }; + } + }; + + return ( + + + + + ); + })} + +
Evaluation GoalYour Self-Evaluation
+ {goal} + + +
+
+
+ )} +
+ ); +}; + +export default SelfEvaluation; diff --git a/client/src/services/EnrollmentService.ts b/client/src/services/EnrollmentService.ts index d073a9bf..cdc65f4e 100644 --- a/client/src/services/EnrollmentService.ts +++ b/client/src/services/EnrollmentService.ts @@ -78,6 +78,28 @@ class EnrollmentService { throw error; } } + + static async updateSelfEvaluation(classId: string, studentCPF: string, goal: string, grade: string): Promise { + try { + const response = await fetch(`${API_BASE_URL}/api/classes/${classId}/enrollments/${studentCPF}/selfEvaluation`, { + method: 'PUT', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ goal, grade }), + }); + + if (!response.ok) { + const errorData = await response.json(); + throw new Error(errorData.error || 'Failed to update self-evaluation'); + } + + return response.json(); + } catch (error) { + console.error('Error updating self-evaluation:', error); + throw error; + } + } } export default EnrollmentService; \ No newline at end of file diff --git a/client/src/types/Enrollment.ts b/client/src/types/Enrollment.ts index 40960d32..70e5c654 100644 --- a/client/src/types/Enrollment.ts +++ b/client/src/types/Enrollment.ts @@ -4,13 +4,16 @@ import { Evaluation } from './Evaluation'; export interface Enrollment { student: Student; evaluations: Evaluation[]; + selfEvaluations: Evaluation[]; } export interface CreateEnrollmentRequest { studentCPF: string; evaluations?: Evaluation[]; + selfEvaluations?: Evaluation[]; } export interface UpdateEnrollmentRequest { evaluations?: Evaluation[]; + selfEvaluations?: Evaluation[]; } \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 2c5b6ff7..e0f56f60 100644 --- a/package-lock.json +++ b/package-lock.json @@ -63,6 +63,7 @@ "client/node_modules/@babel/core": { "version": "7.28.5", "license": "MIT", + "peer": true, "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.5", @@ -615,6 +616,7 @@ "client/node_modules/@babel/plugin-syntax-flow": { "version": "7.27.1", "license": "MIT", + "peer": true, "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, @@ -1382,6 +1384,7 @@ "client/node_modules/@babel/plugin-transform-react-jsx": { "version": "7.27.1", "license": "MIT", + "peer": true, "dependencies": { "@babel/helper-annotate-as-pure": "^7.27.1", "@babel/helper-module-imports": "^7.27.1", @@ -3137,6 +3140,7 @@ "version": "18.3.26", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@types/prop-types": "*", "csstype": "^3.0.2" @@ -3208,6 +3212,7 @@ "client/node_modules/@typescript-eslint/eslint-plugin": { "version": "5.62.0", "license": "MIT", + "peer": true, "dependencies": { "@eslint-community/regexpp": "^4.4.0", "@typescript-eslint/scope-manager": "5.62.0", @@ -3257,6 +3262,7 @@ "client/node_modules/@typescript-eslint/parser": { "version": "5.62.0", "license": "BSD-2-Clause", + "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "5.62.0", "@typescript-eslint/types": "5.62.0", @@ -3565,6 +3571,7 @@ "client/node_modules/acorn": { "version": "8.15.0", "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -3645,6 +3652,7 @@ "client/node_modules/ajv": { "version": "6.12.6", "license": "MIT", + "peer": true, "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", @@ -4396,6 +4404,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "baseline-browser-mapping": "^2.8.19", "caniuse-lite": "^1.0.30001751", @@ -5870,6 +5879,7 @@ "client/node_modules/eslint": { "version": "8.57.1", "license": "MIT", + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", @@ -8178,6 +8188,7 @@ "client/node_modules/jest": { "version": "27.5.1", "license": "MIT", + "peer": true, "dependencies": { "@jest/core": "^27.5.1", "import-local": "^3.0.2", @@ -8971,6 +8982,7 @@ "client/node_modules/jiti": { "version": "1.21.7", "license": "MIT", + "peer": true, "bin": { "jiti": "bin/jiti.js" } @@ -10026,6 +10038,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", @@ -11024,6 +11037,7 @@ "client/node_modules/postcss-selector-parser": { "version": "6.1.2", "license": "MIT", + "peer": true, "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" @@ -11318,6 +11332,7 @@ "client/node_modules/react": { "version": "18.3.1", "license": "MIT", + "peer": true, "dependencies": { "loose-envify": "^1.1.0" }, @@ -11455,6 +11470,7 @@ "client/node_modules/react-refresh": { "version": "0.11.0", "license": "MIT", + "peer": true, "engines": { "node": ">=0.10.0" } @@ -11804,6 +11820,7 @@ "client/node_modules/rollup": { "version": "2.79.2", "license": "MIT", + "peer": true, "bin": { "rollup": "dist/bin/rollup" }, @@ -11998,6 +12015,7 @@ "client/node_modules/schema-utils/node_modules/ajv": { "version": "8.17.1", "license": "MIT", + "peer": true, "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", @@ -13333,6 +13351,7 @@ "client/node_modules/type-fest": { "version": "0.20.2", "license": "(MIT OR CC0-1.0)", + "peer": true, "engines": { "node": ">=10" }, @@ -13416,6 +13435,7 @@ "client/node_modules/typescript": { "version": "4.9.5", "license": "Apache-2.0", + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -13661,6 +13681,7 @@ "client/node_modules/webpack": { "version": "5.102.1", "license": "MIT", + "peer": true, "dependencies": { "@types/eslint-scope": "^3.7.7", "@types/estree": "^1.0.8", @@ -13728,6 +13749,7 @@ "client/node_modules/webpack-dev-server": { "version": "4.15.2", "license": "MIT", + "peer": true, "dependencies": { "@types/bonjour": "^3.5.9", "@types/connect-history-api-fallback": "^1.3.5", @@ -14090,6 +14112,7 @@ "client/node_modules/workbox-build/node_modules/ajv": { "version": "8.17.1", "license": "MIT", + "peer": true, "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", @@ -15158,6 +15181,7 @@ "version": "20.19.24", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "undici-types": "~6.21.0" } @@ -16268,6 +16292,7 @@ "version": "5.9.3", "dev": true, "license": "Apache-2.0", + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" diff --git a/server/data/app-data.json b/server/data/app-data.json index 96a00106..a472520d 100644 --- a/server/data/app-data.json +++ b/server/data/app-data.json @@ -54,6 +54,32 @@ "goal": "Refactoring", "grade": "MA" } + ], + "selfEvaluations": [ + { + "goal": "Requirements", + "grade": "MPA" + }, + { + "goal": "Configuration Management", + "grade": "MPA" + }, + { + "goal": "Project Management", + "grade": "MANA" + }, + { + "goal": "Design", + "grade": "MA" + }, + { + "goal": "Tests", + "grade": "MA" + }, + { + "goal": "Refactoring", + "grade": "MA" + } ] }, { @@ -83,6 +109,32 @@ "goal": "Tests", "grade": "MPA" } + ], + "selfEvaluations": [ + { + "goal": "Requirements", + "grade": "MPA" + }, + { + "goal": "Configuration Management", + "grade": "MPA" + }, + { + "goal": "Project Management", + "grade": "MA" + }, + { + "goal": "Design", + "grade": "MPA" + }, + { + "goal": "Tests", + "grade": "MA" + }, + { + "goal": "Refactoring", + "grade": "MA" + } ] }, { @@ -100,6 +152,32 @@ "goal": "Project Management", "grade": "MPA" } + ], + "selfEvaluations": [ + { + "goal": "Requirements", + "grade": "MA" + }, + { + "goal": "Configuration Management", + "grade": "MPA" + }, + { + "goal": "Project Management", + "grade": "MA" + }, + { + "goal": "Design", + "grade": "MANA" + }, + { + "goal": "Tests", + "grade": "MPA" + }, + { + "goal": "Refactoring", + "grade": "MA" + } ] } ] @@ -114,27 +192,53 @@ "evaluations": [ { "goal": "Requirements", - "grade": "MPA" + "grade": "MANA" }, { "goal": "Configuration Management", - "grade": "MA" + "grade": "MANA" }, { "goal": "Project Management", - "grade": "MPA" + "grade": "MANA" }, { "goal": "Design", - "grade": "MA" + "grade": "MANA" }, { "goal": "Refactoring", + "grade": "MANA" + }, + { + "goal": "Tests", + "grade": "MANA" + } + ], + "selfEvaluations": [ + { + "goal": "Requirements", + "grade": "MA" + }, + { + "goal": "Configuration Management", + "grade": "MA" + }, + { + "goal": "Project Management", + "grade": "MA" + }, + { + "goal": "Design", "grade": "MA" }, { "goal": "Tests", "grade": "MA" + }, + { + "goal": "Refactoring", + "grade": "MA" } ] }, @@ -165,7 +269,8 @@ "goal": "Refactoring", "grade": "MA" } - ] + ], + "selfEvaluations": [] } ] } diff --git a/server/package-lock.json b/server/package-lock.json index 5e8d66bb..9648e5fb 100644 --- a/server/package-lock.json +++ b/server/package-lock.json @@ -178,6 +178,7 @@ "integrity": "sha512-FE5u0ezmi6y9OZEzlJfg37mqqf6ZDSF2V/NLjUyGrR9uTZ7Sb9F7bLNZ03S4XVUNRWGA7Ck4c1kK+YnuWjl+DA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "undici-types": "~6.21.0" } @@ -1769,6 +1770,7 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, "license": "Apache-2.0", + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" From b55765c05fe035b6b11319899e821b636d58f8d8 Mon Sep 17 00:00:00 2001 From: 4POL07 Date: Sun, 16 Nov 2025 16:39:05 -0300 Subject: [PATCH 07/25] feat(discrepancy): add InfoButton --- client/src/components/InfoButton.tsx | 60 ++++++++++++++++++++++++++++ 1 file changed, 60 insertions(+) create mode 100644 client/src/components/InfoButton.tsx diff --git a/client/src/components/InfoButton.tsx b/client/src/components/InfoButton.tsx new file mode 100644 index 00000000..f70f24be --- /dev/null +++ b/client/src/components/InfoButton.tsx @@ -0,0 +1,60 @@ +import React, { useState } from "react"; + +type InfoButtonProps = { + text: string; +}; + +const InfoButton: React.FC = ({ text }) => { + const [show, setShow] = useState(false); + + return ( +
+
setShow(true)} + onMouseLeave={() => setShow(false)} + style={{ + width: "16px", + height: "16px", + cursor: "pointer", + display: "flex", + alignItems: "center", + justifyContent: "center", + }} + > + + + + + +
+ + {show && ( +
+ {text} +
+ )} +
+ ); +}; + +export default InfoButton; From f6d16091f67a8c68e3cb773a8af4e3136fb9bafd Mon Sep 17 00:00:00 2001 From: 4POL07 Date: Sun, 30 Nov 2025 17:25:02 -0300 Subject: [PATCH 08/25] =?UTF-8?q?fix(InfoButton):=20Garante=20que=20o=20co?= =?UTF-8?q?nte=C3=BAdo=20n=C3=A3o=20fique=20atr=C3=A1s=20das=20c=C3=A9lula?= =?UTF-8?q?s=20da=20tabela?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- client/src/components/InfoButton.tsx | 63 +++++++++++++++++++++------- 1 file changed, 47 insertions(+), 16 deletions(-) diff --git a/client/src/components/InfoButton.tsx b/client/src/components/InfoButton.tsx index f70f24be..4bf51436 100644 --- a/client/src/components/InfoButton.tsx +++ b/client/src/components/InfoButton.tsx @@ -1,4 +1,4 @@ -import React, { useState } from "react"; +import React, { useState, useRef, useEffect } from "react"; type InfoButtonProps = { text: string; @@ -6,9 +6,38 @@ type InfoButtonProps = { const InfoButton: React.FC = ({ text }) => { const [show, setShow] = useState(false); + const [side, setSide] = useState<"right" | "left">("right"); + + const tooltipRef = useRef(null); + const wrapperRef = useRef(null); + + useEffect(() => { + if (!show || !tooltipRef.current || !wrapperRef.current) return; + + const tooltip = tooltipRef.current.getBoundingClientRect(); + const anchor = wrapperRef.current.getBoundingClientRect(); + const table = wrapperRef.current.closest("table")?.getBoundingClientRect(); + + if (!table) return; + + if (anchor.right + tooltip.width < table.right) { + setSide("right"); + } else if (anchor.left - tooltip.width > table.left) { + setSide("left"); + } else { + setSide("right"); + } + }, [show]); return ( -
+
setShow(true)} onMouseLeave={() => setShow(false)} @@ -19,15 +48,10 @@ const InfoButton: React.FC = ({ text }) => { display: "flex", alignItems: "center", justifyContent: "center", + zIndex: 5 }} > - + @@ -36,18 +60,25 @@ const InfoButton: React.FC = ({ text }) => { {show && (
{text} From 6eef0ff80e9396b4eb0c1cc77e1fad83565ac80e Mon Sep 17 00:00:00 2001 From: 4POL07 Date: Sun, 30 Nov 2025 17:47:06 -0300 Subject: [PATCH 09/25] feat(self-evaluation): Implement a comparison logic between evaluation and self-evaluation. --- client/src/components/Evaluations.tsx | 55 +++++++++++++++++---------- 1 file changed, 34 insertions(+), 21 deletions(-) diff --git a/client/src/components/Evaluations.tsx b/client/src/components/Evaluations.tsx index 6a1a59da..c08fbb44 100644 --- a/client/src/components/Evaluations.tsx +++ b/client/src/components/Evaluations.tsx @@ -24,7 +24,7 @@ const Evaluations: React.FC = ({ onError }) => { // Predefined evaluation goals const evaluationGoals = [ 'Requirements', - 'Configuration Management', + 'Configuration Management', 'Project Management', 'Design', 'Tests', @@ -89,6 +89,19 @@ const Evaluations: React.FC = ({ onError }) => { return 'discrepancy'; }; + const compareGoal = (teacherEval: string | null | undefined, selfEval: string | null | undefined): boolean => { + // Hierarquia das notas + const hierarchy: Record = { MA: 3, MPA: 2, MANA: 1 }; + + const t = teacherEval && hierarchy[teacherEval] ? hierarchy[teacherEval] : null; + const s = selfEval && hierarchy[selfEval] ? hierarchy[selfEval] : null; + + // Sem discrepância se qualquer nota estiver vazia ou inválida + if (t === null || s === null) return false; + + return t < s; + }; + if (isLoading) { return (
@@ -103,7 +116,7 @@ const Evaluations: React.FC = ({ onError }) => { return (

Evaluations

- + {/* Class Selection */}
@@ -123,10 +136,10 @@ const Evaluations: React.FC = ({ onError }) => {
{!selectedClass && ( -
= ({ onError }) => { )} {selectedClass && selectedClass.enrollments.length === 0 && ( -
= ({ onError }) => { {selectedClass.enrollments.map(enrollment => { const student = enrollment.student; - + // Create a map of evaluations for quick lookup const studentEvaluations = enrollment.evaluations.reduce((acc, evaluation) => { acc[evaluation.goal] = evaluation.grade; return acc; - }, {} as {[goal: string]: string}); + }, {} as { [goal: string]: string }); return ( {student.name} {evaluationGoals.map(goal => { const currentGrade = studentEvaluations[goal] || ''; - + return (