From e0a34f4c59cd16aeee27417a55b7c4bf22b25c7b Mon Sep 17 00:00:00 2001 From: Oliver Pelly Date: Wed, 8 Apr 2026 17:10:34 -0700 Subject: [PATCH] consolidated API endpoint for student data loading --- api/src/Controllers/StudentController.cs | 46 ++++++++++ .../dbProgressEventBenchmarkRow.cs | 7 ++ .../dbProgressEventWithGoalRow.cs | 10 +++ .../Repositories/StudentRepository.cs | 72 +++++++++++++++ .../StudentFullProfileResponse.cs | 25 ++++++ .../procedures/sp_Student_GetFullProfile.sql | 75 ++++++++++++++++ .../edit-event-modal/edit-event-modal.ts | 14 ++- .../components/workspace/workspace.html | 5 +- .../desktop/components/workspace/workspace.ts | 83 +++++++++-------- .../classes/student-full-profile.dto.ts | 24 +++++ .../app/shared/services/student.service.ts | 88 ++++++++----------- 11 files changed, 348 insertions(+), 101 deletions(-) create mode 100644 api/src/DataAccess/Models/DatabaseObjects/dbProgressEventBenchmarkRow.cs create mode 100644 api/src/DataAccess/Models/DatabaseObjects/dbProgressEventWithGoalRow.cs create mode 100644 api/src/Models/ResponseTypes/StudentFullProfileResponse.cs create mode 100644 db/Objects/procedures/sp_Student_GetFullProfile.sql create mode 100644 ui/winstudentgoaltracker/src/app/shared/classes/student-full-profile.dto.ts diff --git a/api/src/Controllers/StudentController.cs b/api/src/Controllers/StudentController.cs index a2a5470..99dc6e7 100644 --- a/api/src/Controllers/StudentController.cs +++ b/api/src/Controllers/StudentController.cs @@ -97,6 +97,52 @@ public class StudentController : BaseController }); } + [HttpGet("{idStudent:guid}/full")] + [Authorize(Roles = $"{UserRoles.Teacher},{UserRoles.Paraeducator},{UserRoles.ProgramAdmin}")] + [ProducesResponseType(typeof(ResponseResult), StatusCodes.Status200OK)] + [ProducesResponseType(typeof(ResponseResult), StatusCodes.Status404NotFound)] + public async Task>> GetFullProfile(Guid idStudent) + { + var (userId, email, programId, role, error) = GetProgramUserFromClaims(); + if (error is not null) + { + return error; + } + + var students = await _studentRepository.GetMyStudentsAsync(userId, programId, role, "all"); + + if (!students.Select(s => s.StudentId).Contains(idStudent)) + { + return NotFound(new ResponseResult + { + Success = false, + Message = "Student not found." + }); + } + + var profile = await _studentRepository.GetFullProfileAsync(idStudent); + if (profile is null) + { + return NotFound(new ResponseResult + { + Success = false, + Message = "Student not found." + }); + } + + // Enrich with ownership info from the authorization query. + var match = students.Single(s => s.StudentId == idStudent); + profile.Student.OwnerName = match.OwnerName; + profile.Student.IsMine = match.IsMine; + + return Ok(new ResponseResult + { + Success = true, + Message = "Student profile retrieved successfully.", + Data = profile + }); + } + [HttpGet("{idStudent:guid}/goals")] [Authorize(Roles = $"{UserRoles.Teacher},{UserRoles.Paraeducator},{UserRoles.ProgramAdmin}")] [ProducesResponseType(typeof(ResponseResult), StatusCodes.Status200OK)] diff --git a/api/src/DataAccess/Models/DatabaseObjects/dbProgressEventBenchmarkRow.cs b/api/src/DataAccess/Models/DatabaseObjects/dbProgressEventBenchmarkRow.cs new file mode 100644 index 0000000..355d4c5 --- /dev/null +++ b/api/src/DataAccess/Models/DatabaseObjects/dbProgressEventBenchmarkRow.cs @@ -0,0 +1,7 @@ +namespace WinStudentGoalTracker.DataAccess; + +public class dbProgressEventBenchmarkRow +{ + public required Guid ProgressEventId { get; set; } + public required Guid BenchmarkId { get; set; } +} diff --git a/api/src/DataAccess/Models/DatabaseObjects/dbProgressEventWithGoalRow.cs b/api/src/DataAccess/Models/DatabaseObjects/dbProgressEventWithGoalRow.cs new file mode 100644 index 0000000..5727b61 --- /dev/null +++ b/api/src/DataAccess/Models/DatabaseObjects/dbProgressEventWithGoalRow.cs @@ -0,0 +1,10 @@ +namespace WinStudentGoalTracker.DataAccess; + +public class dbProgressEventWithGoalRow +{ + public required Guid ProgressEventId { get; set; } + public required Guid GoalId { get; set; } + public string? Content { get; set; } + public DateTime? CreatedAt { get; set; } + public string? CreatedByName { get; set; } +} diff --git a/api/src/DataAccess/Repositories/StudentRepository.cs b/api/src/DataAccess/Repositories/StudentRepository.cs index 5d970c9..6666c8b 100644 --- a/api/src/DataAccess/Repositories/StudentRepository.cs +++ b/api/src/DataAccess/Repositories/StudentRepository.cs @@ -367,6 +367,78 @@ public class StudentRepository return rowsAffected > 0; } + // ***************************************************************** + // Returns a full student profile: student card, goals, benchmarks, + // progress events, and benchmark/event associations in one call. + // ***************************************************************** + public async Task GetFullProfileAsync(Guid idStudent) + { + using var db = Connection; + using var multi = await db.QueryMultipleAsync( + "sp_Student_GetFullProfile", + new { p_id_student = idStudent.ToString() }, + commandType: CommandType.StoredProcedure); + + // Result set 1: Student card + var student = await multi.ReadSingleOrDefaultAsync(); + if (student is null) return null; + + // Result set 2: Goals + var goalRows = (await multi.ReadAsync()).ToList(); + + // Result set 3: Benchmarks + var benchmarkRows = (await multi.ReadAsync()).ToList(); + + // Result set 4: Progress events + var eventRows = (await multi.ReadAsync()).ToList(); + + // Result set 5: Benchmark/event associations + var linkRows = (await multi.ReadAsync()).ToList(); + + return new StudentFullProfileResponse + { + Student = student, + Goals = goalRows.Select(r => new StudentGoalItem + { + GoalId = r.GoalId, + GoalParentId = r.GoalParentId, + Description = r.Description, + Category = r.Category, + Baseline = r.Baseline, + TargetCompletionDate = r.TargetCompletionDate, + CloseDate = r.CloseDate, + Achieved = r.Achieved, + CloseNotes = r.CloseNotes, + ProgressEventCount = r.ProgressEventCount, + BenchmarkCount = r.BenchmarkCount + }).ToList(), + Benchmarks = benchmarkRows.Select(r => new StudentBenchmarkItem + { + BenchmarkId = r.BenchmarkId, + GoalId = r.GoalId, + GoalCategory = r.GoalCategory, + Benchmark = r.Benchmark, + ShortName = r.ShortName, + CreatedByName = r.CreatedByName, + CreatedAt = r.CreatedAt, + UpdatedAt = r.UpdatedAt + }).ToList(), + ProgressEvents = eventRows.Select(r => new ProgressEventWithGoalResponse + { + ProgressEventId = r.ProgressEventId, + GoalId = r.GoalId, + Content = r.Content, + CreatedAt = r.CreatedAt, + CreatedByName = r.CreatedByName + }).ToList(), + ProgressEventBenchmarks = linkRows.Select(r => new ProgressEventBenchmarkLink + { + ProgressEventId = r.ProgressEventId, + BenchmarkId = r.BenchmarkId + }).ToList() + }; + } + // ***************************************************************** // Returns a full progress report for a student within the given // date range. Calls sp_ProgressReport_GetByStudentId which returns diff --git a/api/src/Models/ResponseTypes/StudentFullProfileResponse.cs b/api/src/Models/ResponseTypes/StudentFullProfileResponse.cs new file mode 100644 index 0000000..420ece2 --- /dev/null +++ b/api/src/Models/ResponseTypes/StudentFullProfileResponse.cs @@ -0,0 +1,25 @@ +namespace WinStudentGoalTracker.Models; + +public class StudentFullProfileResponse +{ + public required StudentResponse Student { get; set; } + public List Goals { get; set; } = []; + public List Benchmarks { get; set; } = []; + public List ProgressEvents { get; set; } = []; + public List ProgressEventBenchmarks { get; set; } = []; +} + +public class ProgressEventWithGoalResponse +{ + public Guid ProgressEventId { get; set; } + public Guid GoalId { get; set; } + public string? Content { get; set; } + public DateTime? CreatedAt { get; set; } + public string? CreatedByName { get; set; } +} + +public class ProgressEventBenchmarkLink +{ + public Guid ProgressEventId { get; set; } + public Guid BenchmarkId { get; set; } +} diff --git a/db/Objects/procedures/sp_Student_GetFullProfile.sql b/db/Objects/procedures/sp_Student_GetFullProfile.sql new file mode 100644 index 0000000..7eea52f --- /dev/null +++ b/db/Objects/procedures/sp_Student_GetFullProfile.sql @@ -0,0 +1,75 @@ +DELIMITER ;; +CREATE DEFINER=`root`@`%` PROCEDURE `sp_Student_GetFullProfile`(IN p_id_student CHAR(36)) +BEGIN + -- Result set 1: Student card + SELECT + studentId, + identifier, + nextIepDate, + firstEntryDate, + lastEntryDate, + goalCount, + progressEventCount, + benchmarkCount + FROM v_student_card + WHERE studentId = p_id_student + LIMIT 1; + + -- Result set 2: Goals + SELECT + s.`identifier` AS `studentIdentifier`, + vc.`goalId`, + vc.`goalParentId`, + vc.`description`, + vc.`category`, + vc.`baseline`, + vc.`targetCompletionDate`, + vc.`closeDate`, + vc.`achieved`, + vc.`closeNotes`, + vc.`progressEventCount`, + vc.`benchmarkCount` + FROM `v_goal_card` vc + INNER JOIN `student` s ON s.`id_student` = vc.`studentId` + WHERE vc.`studentId` = p_id_student + ORDER BY vc.`goalId`; + + -- Result set 3: Benchmarks + SELECT + s.`identifier` AS `studentIdentifier`, + b.`id_benchmark` AS `benchmarkId`, + b.`id_goal` AS `goalId`, + g.`category` AS `goalCategory`, + b.`benchmark` AS `benchmark`, + b.`short_name` AS `shortName`, + u.`name` AS `createdByName`, + b.`created_at` AS `createdAt`, + b.`updated_at` AS `updatedAt` + FROM `benchmark` b + INNER JOIN `goal` g ON g.`id_goal` = b.`id_goal` + INNER JOIN `student` s ON s.`id_student` = g.`id_student` + LEFT JOIN `user` u ON u.`id_user` = b.`id_user_created` + WHERE g.`id_student` = p_id_student + ORDER BY b.`created_at` DESC; + + -- Result set 4: Progress events (all goals for this student) + SELECT + vc.`progressEventId`, + vc.`goalId`, + vc.`content`, + vc.`createdAt`, + vc.`createdByName` + FROM `v_progress_event_card` vc + WHERE vc.`studentId` = p_id_student + ORDER BY vc.`createdAt` DESC; + + -- Result set 5: Benchmark/progress-event associations + SELECT + peb.`id_progress_event` AS `progressEventId`, + peb.`id_benchmark` AS `benchmarkId` + FROM `progress_event_benchmark` peb + INNER JOIN `progress_event` pe ON pe.`id_progress_event` = peb.`id_progress_event` + INNER JOIN `goal` g ON g.`id_goal` = pe.`id_goal` + WHERE g.`id_student` = p_id_student; +END;; +DELIMITER ; diff --git a/ui/winstudentgoaltracker/src/app/desktop/components/edit-event-modal/edit-event-modal.ts b/ui/winstudentgoaltracker/src/app/desktop/components/edit-event-modal/edit-event-modal.ts index 7e4df09..59acd86 100644 --- a/ui/winstudentgoaltracker/src/app/desktop/components/edit-event-modal/edit-event-modal.ts +++ b/ui/winstudentgoaltracker/src/app/desktop/components/edit-event-modal/edit-event-modal.ts @@ -3,7 +3,7 @@ import { FormsModule } from '@angular/forms'; import { ModalShell } from '../modal-shell/modal-shell'; import { StudentService } from '../../../shared/services/student.service'; import { BenchmarkDto } from '../../../shared/classes/benchmark.dto'; -import { ProgressEventDto } from '../../../shared/classes/progress-event.dto'; +import { ProgressEventWithGoalDto } from '../../../shared/classes/student-full-profile.dto'; import { GOAL_COLOR } from '../../../shared/classes/category-colors'; @Component({ @@ -19,8 +19,10 @@ export class EditEventModal { readonly goalId = input.required(); readonly benchmarks = input([]); + /** Benchmark IDs already associated with this event (from cached profile). */ + readonly eventBenchmarkIds = input([]); /** null for new event, populated for edit */ - readonly event = input(null); + readonly event = input(null); readonly saved = output(); readonly closed = output(); @@ -30,15 +32,11 @@ export class EditEventModal { protected content = ''; - async ngOnInit() { + ngOnInit() { const ev = this.event(); if (ev) { this.content = ev.content; - // Load existing benchmark associations - const result = await this.studentService.getProgressEventBenchmarks(ev.progressEventId); - if (result.success && result.payload) { - this.selectedBenchmarkIds.set(new Set(result.payload)); - } + this.selectedBenchmarkIds.set(new Set(this.eventBenchmarkIds())); } } diff --git a/ui/winstudentgoaltracker/src/app/desktop/components/workspace/workspace.html b/ui/winstudentgoaltracker/src/app/desktop/components/workspace/workspace.html index 4eeb9d1..9276a5b 100644 --- a/ui/winstudentgoaltracker/src/app/desktop/components/workspace/workspace.html +++ b/ui/winstudentgoaltracker/src/app/desktop/components/workspace/workspace.html @@ -19,6 +19,7 @@ @if (showEditEventModal()) { } @@ -68,7 +69,7 @@ @@ -98,7 +99,7 @@
- @for (ev of sortedProgressEvents(); track ev.progressEventId) { + @for (ev of goalProgressEvents(); track ev.progressEventId) {
diff --git a/ui/winstudentgoaltracker/src/app/desktop/components/workspace/workspace.ts b/ui/winstudentgoaltracker/src/app/desktop/components/workspace/workspace.ts index d3037ce..e4e96b6 100644 --- a/ui/winstudentgoaltracker/src/app/desktop/components/workspace/workspace.ts +++ b/ui/winstudentgoaltracker/src/app/desktop/components/workspace/workspace.ts @@ -4,7 +4,7 @@ import { StudentService } from '../../../shared/services/student.service'; import { StudentCardDto } from '../../../shared/classes/student-card.dto'; import { StudentGoalItem } from '../../../shared/classes/student-goal'; import { BenchmarkDto } from '../../../shared/classes/benchmark.dto'; -import { ProgressEventDto } from '../../../shared/classes/progress-event.dto'; +import { StudentFullProfileDto, ProgressEventWithGoalDto, ProgressEventBenchmarkLink } from '../../../shared/classes/student-full-profile.dto'; import { GoalModal } from '../goal-modal/goal-modal'; import { EditBenchmarkModal } from '../edit-benchmark-modal/edit-benchmark-modal'; import { EditEventModal } from '../edit-event-modal/edit-event-modal'; @@ -50,6 +50,7 @@ export class Workspace { if (initialized) { const id = untracked(() => this.studentId()); if (id) { + this.studentService.invalidateProfile(id); this.loadStudentData(id); } } @@ -67,14 +68,15 @@ export class Workspace { protected readonly student = signal(null); protected readonly goals = signal([]); protected readonly benchmarks = signal([]); - protected readonly progressEvents = signal([]); + protected readonly progressEvents = signal([]); + protected readonly progressEventBenchmarks = signal([]); protected readonly selectedGoalId = signal(null); protected readonly activeTab = signal('benchmarks'); // Modal states protected readonly showGoalModal = signal(null); protected readonly showEditBenchmarkModal = signal(null); - protected readonly showEditEventModal = signal(null); + protected readonly showEditEventModal = signal(null); // ************************** Properties *************************** @@ -91,8 +93,11 @@ export class Workspace { return this.benchmarks().filter(b => b.goalId === goalId); }); - protected readonly sortedProgressEvents = computed(() => { - return [...this.progressEvents()] + protected readonly goalProgressEvents = computed(() => { + const goalId = this.selectedGoal()?.goalId; + if (!goalId) return []; + return this.progressEvents() + .filter(e => e.goalId === goalId) .sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()); }); @@ -107,7 +112,6 @@ export class Workspace { onSelectGoal(goalId: string) { this.selectedGoalId.set(goalId); this.activeTab.set('benchmarks'); - this.loadGoalDetails(goalId); this.router.navigate(['/students', this.studentId(), 'goals', goalId]); } @@ -122,7 +126,7 @@ export class Workspace { onGoalSaved() { this.showGoalModal.set(null); - this.loadStudentData(this.studentId()!); + this.refetchProfile(); } onAddGoal() { @@ -132,9 +136,8 @@ export class Workspace { onGoalCreated(goal: StudentGoalItem) { this.showGoalModal.set(null); this.studentService.notifyDataChanged(); - this.loadStudentData(this.studentId()!).then(() => { + this.refetchProfile().then(() => { this.selectedGoalId.set(goal.goalId); - this.loadGoalDetails(goal.goalId); }); } @@ -144,7 +147,7 @@ export class Workspace { onEditBenchmarkSaved() { this.showEditBenchmarkModal.set(null); - this.loadStudentData(this.studentId()!); + this.refetchProfile(); } onAddBenchmark() { @@ -155,15 +158,23 @@ export class Workspace { this.showEditEventModal.set('new'); } - onEditEvent(ev: ProgressEventDto) { + onEditEvent(ev: ProgressEventWithGoalDto) { this.showEditEventModal.set(ev); } onEventSaved() { this.showEditEventModal.set(null); - if (this.selectedGoal()) { - this.loadGoalDetails(this.selectedGoal()!.goalId); - } + this.refetchProfile(); + } + + // ***************************************************************** + // Returns the benchmark IDs associated with a given progress event, + // read from the cached profile data. + // ***************************************************************** + getBenchmarkIdsForEvent(progressEventId: string): string[] { + return this.progressEventBenchmarks() + .filter(link => link.progressEventId === progressEventId) + .map(link => link.benchmarkId); } // ************************ Formatting Helpers ********************** @@ -177,39 +188,27 @@ export class Workspace { // ********************** Support Procedures *********************** private async loadStudentData(studentId: string) { - const [studentResult, goalsResult, bmResult] = await Promise.all([ - this.studentService.getStudentById(studentId), - this.studentService.getGoalsForStudent(studentId), - this.studentService.getBenchmarksForStudent(studentId), - ]); + const result = await this.studentService.getFullProfile(studentId); - if (studentResult.success && studentResult.payload) { - this.student.set(studentResult.payload); - } + if (!result.success || !result.payload) return; - if (goalsResult.success && goalsResult.payload) { - this.goals.set(goalsResult.payload.goals); - // Auto-select first goal if none selected - if (!this.selectedGoalId() && goalsResult.payload.goals.length > 0) { - this.selectedGoalId.set(goalsResult.payload.goals[0].goalId); - } - } + const profile = result.payload; + this.student.set(profile.student); + this.goals.set(profile.goals); + this.benchmarks.set(profile.benchmarks); + this.progressEvents.set(profile.progressEvents); + this.progressEventBenchmarks.set(profile.progressEventBenchmarks); - if (bmResult.success && bmResult.payload) { - this.benchmarks.set(bmResult.payload.benchmarks); - } - - // Load progress events for selected goal - const goalId = this.selectedGoalId(); - if (goalId) { - this.loadGoalDetails(goalId); + // Auto-select first goal if none selected + if (!this.selectedGoalId() && profile.goals.length > 0) { + this.selectedGoalId.set(profile.goals[0].goalId); } } - private async loadGoalDetails(goalId: string) { - const result = await this.studentService.getProgressEventsForGoal(goalId); - if (result.success) { - this.progressEvents.set(result.payload ?? []); - } + private async refetchProfile(): Promise { + const id = this.studentId(); + if (!id) return; + this.studentService.invalidateProfile(id); + await this.loadStudentData(id); } } diff --git a/ui/winstudentgoaltracker/src/app/shared/classes/student-full-profile.dto.ts b/ui/winstudentgoaltracker/src/app/shared/classes/student-full-profile.dto.ts new file mode 100644 index 0000000..23e07d4 --- /dev/null +++ b/ui/winstudentgoaltracker/src/app/shared/classes/student-full-profile.dto.ts @@ -0,0 +1,24 @@ +import { StudentCardDto } from './student-card.dto'; +import { StudentGoalItem } from './student-goal'; +import { BenchmarkDto } from './benchmark.dto'; + +export interface StudentFullProfileDto { + student: StudentCardDto; + goals: StudentGoalItem[]; + benchmarks: BenchmarkDto[]; + progressEvents: ProgressEventWithGoalDto[]; + progressEventBenchmarks: ProgressEventBenchmarkLink[]; +} + +export interface ProgressEventWithGoalDto { + progressEventId: string; + goalId: string; + content: string; + createdAt: Date; + createdByName: string; +} + +export interface ProgressEventBenchmarkLink { + progressEventId: string; + benchmarkId: string; +} diff --git a/ui/winstudentgoaltracker/src/app/shared/services/student.service.ts b/ui/winstudentgoaltracker/src/app/shared/services/student.service.ts index 11f7dfd..a785fa5 100644 --- a/ui/winstudentgoaltracker/src/app/shared/services/student.service.ts +++ b/ui/winstudentgoaltracker/src/app/shared/services/student.service.ts @@ -9,9 +9,9 @@ import { CreateStudentDto } from '../classes/create-student.dto'; import { CreateGoalDto } from '../classes/create-goal.dto'; import { StudentCardDto } from '../classes/student-card.dto'; import { StudentGoalSummary, StudentGoalItem } from '../classes/student-goal'; -import { ProgressEventDto } from '../classes/progress-event.dto'; import { StudentBenchmarkSummary } from '../classes/benchmark.dto'; import { StudentProgressReportDto } from '../classes/student-progress-report.dto'; +import { StudentFullProfileDto } from '../classes/student-full-profile.dto'; @Injectable({ providedIn: 'root', @@ -28,6 +28,9 @@ export class StudentService { // Incremented after any data mutation so subscribers can refresh. readonly dataVersion = signal(0); + // Per-student full profile cache. + private readonly profileCache = new Map(); + // Emits targeted label updates for sidebar nodes without a full rebuild. private readonly _sidebarLabelUpdate = new Subject<{ routerLink: string[]; label: string }>(); readonly sidebarLabelUpdate$ = this._sidebarLabelUpdate.asObservable(); @@ -43,6 +46,41 @@ export class StudentService { this.dataVersion.update(v => v + 1); } + // ***************************************************************** + // Returns the full profile for a student. Uses a per-student cache + // so subsequent loads are instant. Call invalidateProfile() after + // mutations to force a fresh fetch. + // ***************************************************************** + async getFullProfile(studentId: string): Promise> { + const cached = this.profileCache.get(studentId); + if (cached) return ApiResult.ok(cached); + + try { + const result = await firstValueFrom( + this.http.get>(`${this.base}/api/Student/${studentId}/full`) + ); + if (result.success && result.data) { + this.profileCache.set(studentId, result.data); + return ApiResult.ok(result.data); + } + return ApiResult.fail(result.message); + } catch (error) { + return ApiResult.fail(describeHttpError(error as HttpErrorResponse)); + } + } + + // ***************************************************************** + // Removes a student's cached profile so the next getFullProfile + // call fetches fresh data. Pass no argument to clear all. + // ***************************************************************** + invalidateProfile(studentId?: string) { + if (studentId) { + this.profileCache.delete(studentId); + } else { + this.profileCache.clear(); + } + } + // ***************************************************************** // Emits a targeted sidebar label update for a specific node, // avoiding the full tree rebuild that notifyDataChanged triggers. @@ -152,38 +190,6 @@ export class StudentService { } } - // ***************************************************************** - // Returns benchmark IDs associated with a progress event. - // ***************************************************************** - async getProgressEventBenchmarks(progressEventId: string): Promise> { - try { - const result = await firstValueFrom( - this.http.get>(`${this.base}/api/Student/progress-events/${progressEventId}/benchmarks`) - ); - return result.success - ? ApiResult.ok(result.data ?? []) - : ApiResult.fail(result.message); - } catch (error) { - return ApiResult.fail(describeHttpError(error as HttpErrorResponse)); - } - } - - // ***************************************************************** - // Returns progress events for a given student goal. - // ***************************************************************** - async getProgressEventsForGoal(goalId: string): Promise> { - try { - const result = await firstValueFrom( - this.http.get>(`${this.base}/api/Student/goals/${goalId}/progress-events`) - ); - return result.success - ? ApiResult.ok(result.data ?? []) - : ApiResult.fail(result.message); - } catch (error) { - return ApiResult.fail(describeHttpError(error as HttpErrorResponse)); - } - } - // ***************************************************************** // Returns a full progress report for a student within a date // range, including goals, events, and benchmark associations. @@ -211,22 +217,6 @@ export class StudentService { // ********************** Support Procedures *********************** - // ***************************************************************** - // Returns a single student by ID. - // ***************************************************************** - async getStudentById(studentId: string): Promise> { - try { - const result = await firstValueFrom( - this.http.get>(`${this.base}/api/Student/${studentId}`) - ); - return result.success && result.data - ? ApiResult.ok(result.data) - : ApiResult.fail(result.message); - } catch (error) { - return ApiResult.fail(describeHttpError(error as HttpErrorResponse)); - } - } - // ***************************************************************** // Updates a student and returns the refreshed student data. // *****************************************************************