From 69f68cc3917026d83a88242042dffad7e97ef654 Mon Sep 17 00:00:00 2001 From: Oliver Pelly Date: Wed, 4 Mar 2026 17:08:03 -0800 Subject: [PATCH] latest --- api/src/Controllers/StudentController.cs | 43 +++++++++++++++++++ .../DatabaseObjects/dbGoalStudentRow.cs | 6 +++ .../DatabaseObjects/dbProgressEventRow.cs | 9 ++++ .../Repositories/StudentRepository.cs | 31 +++++++++++++ .../ResponseTypes/ProgressEventResponse.cs | 9 ++++ .../sp_ProgressEvent_GetByGoalId.sql | 13 ++++++ db/Objects/views/v_progress_event_card.sql | 5 +++ .../components/progress-list/progress-list.ts | 9 ++-- .../app/shared/services/student.service.ts | 17 ++++++++ 9 files changed, 136 insertions(+), 6 deletions(-) create mode 100644 api/src/DataAccess/Models/DatabaseObjects/dbGoalStudentRow.cs create mode 100644 api/src/DataAccess/Models/DatabaseObjects/dbProgressEventRow.cs create mode 100644 api/src/Models/ResponseTypes/ProgressEventResponse.cs create mode 100644 db/Objects/procedures/sp_ProgressEvent_GetByGoalId.sql create mode 100644 db/Objects/views/v_progress_event_card.sql diff --git a/api/src/Controllers/StudentController.cs b/api/src/Controllers/StudentController.cs index ec8bb69..99677e7 100644 --- a/api/src/Controllers/StudentController.cs +++ b/api/src/Controllers/StudentController.cs @@ -246,6 +246,49 @@ public class StudentController : BaseController }); } + [HttpGet("goals/{idGoal:guid}/progress-events")] + [Authorize(Roles = $"{UserRoles.Teacher},{UserRoles.Paraeducator},{UserRoles.ProgramAdmin}")] + [ProducesResponseType(typeof(ResponseResult>), StatusCodes.Status200OK)] + [ProducesResponseType(typeof(ResponseResult>), StatusCodes.Status404NotFound)] + public async Task>>> GetProgressEventsForGoal(Guid idGoal) + { + var (userId, email, programId, role, error) = GetProgramUserFromClaims(); + if (error is not null) + { + return error; + } + + var studentId = await _studentRepository.GetStudentIdForGoalAsync(idGoal); + if (!studentId.HasValue) + { + return NotFound(new ResponseResult> + { + Success = false, + Message = "Goal not found." + }); + } + + var students = await _studentRepository.GetMyStudentsAsync(userId, programId, role); + + if (!students.Select(s => s.StudentId).Contains(studentId.Value)) + { + return NotFound(new ResponseResult> + { + Success = false, + Message = "Goal not found." + }); + } + + var progressEvents = await _studentRepository.GetProgressEventsForGoalAsync(idGoal); + + return Ok(new ResponseResult> + { + Success = true, + Message = "Progress events retrieved successfully.", + Data = progressEvents + }); + } + [HttpPost] [Authorize(Roles = $"{UserRoles.Teacher}")] [ProducesResponseType(typeof(ResponseResult), StatusCodes.Status201Created)] diff --git a/api/src/DataAccess/Models/DatabaseObjects/dbGoalStudentRow.cs b/api/src/DataAccess/Models/DatabaseObjects/dbGoalStudentRow.cs new file mode 100644 index 0000000..1dd1d6f --- /dev/null +++ b/api/src/DataAccess/Models/DatabaseObjects/dbGoalStudentRow.cs @@ -0,0 +1,6 @@ +namespace WinStudentGoalTracker.DataAccess; + +public class dbGoalStudentRow +{ + public Guid StudentId { get; set; } +} diff --git a/api/src/DataAccess/Models/DatabaseObjects/dbProgressEventRow.cs b/api/src/DataAccess/Models/DatabaseObjects/dbProgressEventRow.cs new file mode 100644 index 0000000..cac8e96 --- /dev/null +++ b/api/src/DataAccess/Models/DatabaseObjects/dbProgressEventRow.cs @@ -0,0 +1,9 @@ +namespace WinStudentGoalTracker.DataAccess; + +public class dbProgressEventRow +{ + public required Guid ProgressEventId { 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 3846bc6..9346902 100644 --- a/api/src/DataAccess/Repositories/StudentRepository.cs +++ b/api/src/DataAccess/Repositories/StudentRepository.cs @@ -101,6 +101,37 @@ public class StudentRepository return row is not null; } + public async Task GetStudentIdForGoalAsync(Guid idGoal) + { + using var db = Connection; + var row = await db.QuerySingleOrDefaultAsync( + "sp_Goal_GetById", + new { p_id_goal = idGoal.ToString() }, + commandType: CommandType.StoredProcedure); + + return row?.StudentId; + } + + public async Task> GetProgressEventsForGoalAsync(Guid idGoal) + { + using var db = Connection; + var rows = await db.QueryAsync( + "sp_ProgressEvent_GetByGoalId", + new + { + p_id_goal = idGoal.ToString() + }, + commandType: CommandType.StoredProcedure); + + return rows.Select(r => new ProgressEventResponse + { + ProgressEventId = r.ProgressEventId, + Content = r.Content, + CreatedAt = r.CreatedAt, + CreatedByName = r.CreatedByName + }); + } + public async Task InsertGoalAsync(Guid idStudent, Guid userId, CreateGoalDto dto) { var newGoalId = Guid.NewGuid(); diff --git a/api/src/Models/ResponseTypes/ProgressEventResponse.cs b/api/src/Models/ResponseTypes/ProgressEventResponse.cs new file mode 100644 index 0000000..46b6d92 --- /dev/null +++ b/api/src/Models/ResponseTypes/ProgressEventResponse.cs @@ -0,0 +1,9 @@ +namespace WinStudentGoalTracker.Models; + +public class ProgressEventResponse +{ + public Guid ProgressEventId { get; set; } + public string? Content { get; set; } + public DateTime? CreatedAt { get; set; } + public string? CreatedByName { get; set; } +} diff --git a/db/Objects/procedures/sp_ProgressEvent_GetByGoalId.sql b/db/Objects/procedures/sp_ProgressEvent_GetByGoalId.sql new file mode 100644 index 0000000..6954381 --- /dev/null +++ b/db/Objects/procedures/sp_ProgressEvent_GetByGoalId.sql @@ -0,0 +1,13 @@ +DELIMITER ;; +CREATE DEFINER=`root`@`%` PROCEDURE `sp_ProgressEvent_GetByGoalId`(IN p_id_goal CHAR(36)) +BEGIN + SELECT + vc.`progressEventId`, + vc.`content`, + vc.`createdAt`, + vc.`createdByName` + FROM `v_progress_event_card` vc + WHERE vc.`goalId` = p_id_goal + ORDER BY vc.`createdAt` DESC; +END;; +DELIMITER ; diff --git a/db/Objects/views/v_progress_event_card.sql b/db/Objects/views/v_progress_event_card.sql new file mode 100644 index 0000000..f28d57a --- /dev/null +++ b/db/Objects/views/v_progress_event_card.sql @@ -0,0 +1,5 @@ +CREATE ALGORITHM=UNDEFINED DEFINER=`root`@`%` SQL SECURITY DEFINER VIEW `winstudentgoaltracker`.`v_progress_event_card` AS +select `pe`.`id_progress_event` AS `progressEventId`,`pe`.`id_goal` AS `goalId`,`g`.`id_student` AS `studentId`,`pe`.`content` AS `content`,`pe`.`created_at` AS `createdAt`,`u`.`name` AS `createdByName` +from ((`winstudentgoaltracker`.`progress_event` `pe` +join `winstudentgoaltracker`.`goal` `g` on((`g`.`id_goal` = `pe`.`id_goal`))) +left join `winstudentgoaltracker`.`user` `u` on((`u`.`id_user` = `pe`.`id_user_created`))); diff --git a/ui/winstudentgoaltracker/src/app/desktop/components/progress-list/progress-list.ts b/ui/winstudentgoaltracker/src/app/desktop/components/progress-list/progress-list.ts index da48b3b..fc63f3e 100644 --- a/ui/winstudentgoaltracker/src/app/desktop/components/progress-list/progress-list.ts +++ b/ui/winstudentgoaltracker/src/app/desktop/components/progress-list/progress-list.ts @@ -4,7 +4,6 @@ import { Subject } from 'rxjs'; import { debounceTime } from 'rxjs/operators'; import { ProgressItem } from '../progress-item/progress-item'; import { ProgressEventDto } from '../../../shared/classes/progress-event.dto'; -import { DummyStudentService } from '../../../shared/services/dummy-student.service'; import { StudentService } from '../../../shared/services/student.service'; @Component({ @@ -30,7 +29,6 @@ export class ProgressList implements OnDestroy { // ************************** Declarations ************************* - private readonly dummyService = inject(DummyStudentService); private readonly studentService = inject(StudentService); private readonly route = inject(ActivatedRoute); private readonly router = inject(Router); @@ -97,12 +95,11 @@ export class ProgressList implements OnDestroy { // ********************** Support Procedures *********************** // ***************************************************************** - // Loads progress events for the given goal from the dummy service, - // sorted newest-first by createdAt. - // TODO: Replace DummyStudentService with StudentService + // Loads progress events for the given goal from the API, sorted + // newest-first by createdAt. // ***************************************************************** private loadEvents() { - this.dummyService.getProgressEventsForGoal(this.goalId).then(result => { + this.studentService.getProgressEventsForGoal(this.goalId).then(result => { if (!result.success) { this.errorMessage.set(result.message); } else { diff --git a/ui/winstudentgoaltracker/src/app/shared/services/student.service.ts b/ui/winstudentgoaltracker/src/app/shared/services/student.service.ts index ef5dbab..b7d3534 100644 --- a/ui/winstudentgoaltracker/src/app/shared/services/student.service.ts +++ b/ui/winstudentgoaltracker/src/app/shared/services/student.service.ts @@ -9,6 +9,7 @@ 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'; @Injectable({ providedIn: 'root', @@ -103,6 +104,22 @@ export class StudentService { } } + // ***************************************************************** + // 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)); + } + } + // ************************ Event Handlers ************************* // ********************** Support Procedures ***********************