From f70cbc42d884e6f4e097b4461ba98974268185a4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9s=20Eduardo=20Garc=C3=ADa=20M=C3=A1rquez?= Date: Thu, 8 Jan 2026 00:31:05 -0500 Subject: [PATCH] refactor(frontend): use centralized constants and add JSDoc - Add comprehensive JSDoc to StudentService and EnrollmentService - Replace magic numbers with ENROLLMENT_LIMITS constants - Use RESTRICTION_TRANSLATIONS for enrollment messages - Update CreditsPipe to use MAX_CREDITS constant - Expose maxCredits and maxSubjects in enrollment component This improves code maintainability and developer experience. --- .../app/core/services/enrollment.service.ts | 61 +++++++++++++++++++ .../src/app/core/services/student.service.ts | 49 +++++++++++++++ .../enrollment-page.component.ts | 26 ++++---- .../src/app/shared/pipes/credits.pipe.ts | 10 ++- 4 files changed, 133 insertions(+), 13 deletions(-) diff --git a/src/frontend/src/app/core/services/enrollment.service.ts b/src/frontend/src/app/core/services/enrollment.service.ts index 17744bd..87ac912 100644 --- a/src/frontend/src/app/core/services/enrollment.service.ts +++ b/src/frontend/src/app/core/services/enrollment.service.ts @@ -27,10 +27,30 @@ interface UnenrollResult { unenrollStudent: DeletePayload; } +/** + * Service for managing student enrollments in subjects. + * Handles business rules: max 3 subjects, no duplicate professors. + * + * @example + * ```typescript + * // Enroll student in a subject + * this.enrollmentService.enrollStudent({ studentId: 1, subjectId: 2 }).subscribe(result => { + * if (result.errors?.length) { + * // Handle validation errors + * } + * }); + * ``` + */ @Injectable({ providedIn: 'root' }) export class EnrollmentService { private apollo = inject(Apollo); + /** + * Retrieves a student with their current enrollments. + * + * @param studentId - The student's unique identifier + * @returns Observable with student data including enrollments array + */ getStudentWithEnrollments(studentId: number): Observable<{ data: Student | null; loading: boolean }> { return this.apollo .watchQuery({ @@ -46,6 +66,18 @@ export class EnrollmentService { ); } + /** + * Retrieves subjects available for enrollment. + * Each subject includes availability status based on business rules: + * - Already enrolled: not available + * - Same professor: not available + * - Max subjects reached: not available + * + * Uses network-only to ensure fresh validation from server. + * + * @param studentId - The student's unique identifier + * @returns Observable with available subjects and their restrictions + */ getAvailableSubjects(studentId: number): Observable<{ data: AvailableSubject[]; loading: boolean }> { return this.apollo .watchQuery({ @@ -61,6 +93,13 @@ export class EnrollmentService { ); } + /** + * Retrieves classmates for each subject the student is enrolled in. + * Returns only names of other students (privacy-aware). + * + * @param studentId - The student's unique identifier + * @returns Observable with classmates grouped by subject + */ getClassmates(studentId: number): Observable<{ data: Classmate[]; loading: boolean }> { return this.apollo .watchQuery({ @@ -76,6 +115,18 @@ export class EnrollmentService { ); } + /** + * Enrolls a student in a subject. + * Validates business rules on server: + * - Maximum 3 subjects per student (9 credits) + * - No two subjects with the same professor + * + * Automatically refreshes student data and available subjects on success. + * + * @param input - Enrollment request with studentId and subjectId + * @returns Observable with enrollment result or validation errors + * @throws GraphQL errors: MAX_ENROLLMENTS, SAME_PROFESSOR, DUPLICATE_ENROLLMENT + */ enrollStudent(input: EnrollInput): Observable { return this.apollo .mutate({ @@ -89,6 +140,16 @@ export class EnrollmentService { .pipe(map(result => result.data?.enrollStudent ?? {})); } + /** + * Cancels a student's enrollment in a subject. + * Frees up a slot for another subject enrollment. + * Automatically refreshes student data and available subjects on success. + * + * @param enrollmentId - The enrollment's unique identifier + * @param studentId - The student's ID (needed for cache refresh) + * @returns Observable with success status or errors + * @throws GraphQL errors: ENROLLMENT_NOT_FOUND + */ unenrollStudent(enrollmentId: number, studentId: number): Observable { return this.apollo .mutate({ diff --git a/src/frontend/src/app/core/services/student.service.ts b/src/frontend/src/app/core/services/student.service.ts index 7c679b9..10b98c9 100644 --- a/src/frontend/src/app/core/services/student.service.ts +++ b/src/frontend/src/app/core/services/student.service.ts @@ -28,10 +28,27 @@ interface DeleteStudentResult { deleteStudent: DeletePayload; } +/** + * Service for managing student CRUD operations via GraphQL. + * Provides reactive data access with Apollo Client caching. + * + * @example + * ```typescript + * this.studentService.getStudents().subscribe(({ data, loading }) => { + * this.students = data; + * }); + * ``` + */ @Injectable({ providedIn: 'root' }) export class StudentService { private apollo = inject(Apollo); + /** + * Retrieves all students from the server. + * Uses cache-and-network policy for optimal UX. + * + * @returns Observable emitting students array and loading state + */ getStudents(): Observable<{ data: Student[]; loading: boolean }> { return this.apollo .watchQuery({ @@ -46,6 +63,12 @@ export class StudentService { ); } + /** + * Retrieves a single student by ID. + * + * @param id - The student's unique identifier + * @returns Observable emitting the student (or null if not found) and loading state + */ getStudent(id: number): Observable<{ data: Student | null; loading: boolean }> { return this.apollo .watchQuery({ @@ -61,6 +84,14 @@ export class StudentService { ); } + /** + * Creates a new student in the system. + * Automatically refreshes the students list cache on success. + * + * @param input - Student data (name, email) + * @returns Observable with created student or validation errors + * @throws GraphQL errors for validation failures (VALIDATION_ERROR) + */ createStudent(input: CreateStudentInput): Observable { return this.apollo .mutate({ @@ -71,6 +102,15 @@ export class StudentService { .pipe(map(result => result.data?.createStudent ?? {})); } + /** + * Updates an existing student's information. + * Automatically refreshes the students list cache on success. + * + * @param id - The student's unique identifier + * @param input - Updated student data (name, email) + * @returns Observable with updated student or validation errors + * @throws GraphQL errors: STUDENT_NOT_FOUND, VALIDATION_ERROR + */ updateStudent(id: number, input: UpdateStudentInput): Observable { return this.apollo .mutate({ @@ -81,6 +121,15 @@ export class StudentService { .pipe(map(result => result.data?.updateStudent ?? {})); } + /** + * Deletes a student from the system. + * Also removes all associated enrollments (cascade delete). + * Automatically refreshes the students list cache on success. + * + * @param id - The student's unique identifier + * @returns Observable with success status or errors + * @throws GraphQL errors: STUDENT_NOT_FOUND + */ deleteStudent(id: number): Observable { return this.apollo .mutate({ diff --git a/src/frontend/src/app/features/enrollment/pages/enrollment-page/enrollment-page.component.ts b/src/frontend/src/app/features/enrollment/pages/enrollment-page/enrollment-page.component.ts index 8ad39e0..8db2216 100644 --- a/src/frontend/src/app/features/enrollment/pages/enrollment-page/enrollment-page.component.ts +++ b/src/frontend/src/app/features/enrollment/pages/enrollment-page/enrollment-page.component.ts @@ -5,6 +5,7 @@ import { MatButtonModule } from '@angular/material/button'; import { MatTooltipModule } from '@angular/material/tooltip'; import { EnrollmentService, NotificationService, ErrorHandlerService } from '@core/services'; import { Student, AvailableSubject, Enrollment } from '@core/models'; +import { ENROLLMENT_LIMITS, RESTRICTION_TRANSLATIONS } from '@core/constants'; import { LoadingSpinnerComponent, EmptyStateComponent } from '@shared/index'; @Component({ @@ -54,7 +55,7 @@ import { LoadingSpinnerComponent, EmptyStateComponent } from '@shared/index';
{{ student()?.totalCredits || 0 }} - /9 + /{{ maxCredits }}
@@ -67,7 +68,7 @@ import { LoadingSpinnerComponent, EmptyStateComponent } from '@shared/index';
{{ enrollments().length }} - /3 + /{{ maxSubjects }}
@@ -149,7 +150,7 @@ import { LoadingSpinnerComponent, EmptyStateComponent } from '@shared/index';