From be4873283d1605b51024b11437a3fb655efc6069 Mon Sep 17 00:00:00 2001 From: Oliver Pelly Date: Mon, 2 Mar 2026 15:07:33 -0800 Subject: [PATCH] latest --- api/src/Controllers/StudentController.cs | 29 +++++++++---------- .../Repositories/StudentRepository.cs | 16 +++++----- .../Models/ResponseTypes/StudentResponse.cs | 27 ++++------------- db/Objects/views/v_student_card.sql | 17 +++++++++++ .../components/student-card/student-card.html | 2 +- .../components/student-card/student-card.html | 2 +- .../src/app/mobile/pages/students/students.ts | 2 +- .../app/shared/classes/student-card.dto.ts | 4 +-- .../shared/services/dummy-student.service.ts | 22 +++++++------- 9 files changed, 60 insertions(+), 61 deletions(-) create mode 100644 db/Objects/views/v_student_card.sql diff --git a/api/src/Controllers/StudentController.cs b/api/src/Controllers/StudentController.cs index d118ab7..924233d 100644 --- a/api/src/Controllers/StudentController.cs +++ b/api/src/Controllers/StudentController.cs @@ -27,19 +27,18 @@ public class StudentController : BaseController } var students = await _studentRepository.GetMyStudentsAsync(userId, programId, role); - var response = students.Select(StudentResponse.FromDatabaseModel); return Ok(new ResponseResult> { Success = true, Message = "Students retrieved successfully.", - Data = response + Data = students }); } - // TODO refactor with database changes to ensure - // users who are a district admin are actually associated with a district, and + // TODO refactor with database changes to ensure + // users who are a district admin are actually associated with a district, and // then this endpoint should validate that the requested program is part of the district // Once that is in place, then district admins will be allowed to call this function. [HttpGet("program/{idProgram:guid}")] @@ -55,13 +54,12 @@ public class StudentController : BaseController } var students = await _studentRepository.GetMyStudentsAsync(userId, idProgram, role); - var response = students.Select(StudentResponse.FromDatabaseModel); return Ok(new ResponseResult> { Success = true, Message = "Students retrieved successfully.", - Data = response + Data = students }); } @@ -80,7 +78,7 @@ public class StudentController : BaseController var students = await _studentRepository.GetMyStudentsAsync(userId, programId, role); - if (!students.Select(s => s.IdStudent).Contains(idStudent)) + if (!students.Select(s => s.StudentId).Contains(idStudent)) { return NotFound(new ResponseResult { @@ -88,14 +86,14 @@ public class StudentController : BaseController Message = "Student not found." }); } - - var student = students.Single(s => s.IdStudent == idStudent); + + var student = students.Single(s => s.StudentId == idStudent); return Ok(new ResponseResult { Success = true, Message = "Student retrieved successfully.", - Data = StudentResponse.FromDatabaseModel(student) + Data = student }); } @@ -132,12 +130,11 @@ public class StudentController : BaseController }); } - var response = StudentResponse.FromDatabaseModel(created); - return CreatedAtAction(nameof(GetById), new { idStudent = response.IdStudent }, new ResponseResult + return CreatedAtAction(nameof(GetById), new { idStudent = created.StudentId }, new ResponseResult { Success = true, Message = "Student created successfully.", - Data = response + Data = created }); } @@ -155,7 +152,7 @@ public class StudentController : BaseController var students = await _studentRepository.GetMyStudentsAsync(userId, programId, role); - if (!students.Select(s => s.IdStudent).Contains(idStudent)) + if (!students.Select(s => s.StudentId).Contains(idStudent)) { return NotFound(new ResponseResult { @@ -179,7 +176,7 @@ public class StudentController : BaseController { Success = true, Message = updated ? "Changes applied successfully." : "No changes were applied.", - Data = StudentResponse.FromDatabaseModel(refreshed) + Data = refreshed }); } @@ -197,7 +194,7 @@ public class StudentController : BaseController var students = await _studentRepository.GetMyStudentsAsync(userId, programId, role); - if (!students.Select(s => s.IdStudent).Contains(idStudent)) + if (!students.Select(s => s.StudentId).Contains(idStudent)) { return NotFound(new ResponseResult { diff --git a/api/src/DataAccess/Repositories/StudentRepository.cs b/api/src/DataAccess/Repositories/StudentRepository.cs index 80b261d..b743fcb 100644 --- a/api/src/DataAccess/Repositories/StudentRepository.cs +++ b/api/src/DataAccess/Repositories/StudentRepository.cs @@ -10,7 +10,7 @@ public class StudentRepository { private IDbConnection Connection => new MySqlConnection(DatabaseManager.ConnectionString); - public async Task> GetMyStudentsAsync(Guid userId, Guid programId, string role) + public async Task> GetMyStudentsAsync(Guid userId, Guid programId, string role) { using var db = Connection; using var multi = await db.QueryMultipleAsync( @@ -18,29 +18,29 @@ public class StudentRepository new { p_id_program = programId.ToString(), p_id_user = userId.ToString() }, commandType: CommandType.StoredProcedure); - var students = await multi.ReadAsync(); + var students = await multi.ReadAsync(); var assignments = await multi.ReadAsync(); var myStudents = students.Where(s => - PermissionService.IsAllowed(role, EntityType.Student, PermissionAction.Read , assignments.Any(a => a.IdStudent == s.IdStudent && a.IdUser == userId)) + PermissionService.IsAllowed(role, EntityType.Student, PermissionAction.Read, assignments.Any(a => a.IdStudent == s.StudentId && a.IdUser == userId)) ); return myStudents; } - public async Task GetByIdAsync(Guid idStudent) + public async Task GetByIdAsync(Guid idStudent) { using var db = Connection; - return await db.QuerySingleOrDefaultAsync( + return await db.QuerySingleOrDefaultAsync( "sp_Student_GetById", new { p_id_student = idStudent.ToString() }, commandType: CommandType.StoredProcedure); } - public async Task InsertAsync(CreateStudentDto dto, Guid newStudentGuid, Guid programId, Guid userId) + public async Task InsertAsync(CreateStudentDto dto, Guid newStudentGuid, Guid programId, Guid userId) { using var db = Connection; - return await db.QuerySingleOrDefaultAsync( + await db.ExecuteAsync( "sp_Student_Insert", new { @@ -53,6 +53,8 @@ public class StudentRepository p_expected_grad = dto.ExpectedGrad }, commandType: CommandType.StoredProcedure); + + return await GetByIdAsync(newStudentGuid); } public async Task UpdateAsync(Guid idStudent, UpdateStudentDto dto) diff --git a/api/src/Models/ResponseTypes/StudentResponse.cs b/api/src/Models/ResponseTypes/StudentResponse.cs index c538431..9111d2c 100644 --- a/api/src/Models/ResponseTypes/StudentResponse.cs +++ b/api/src/Models/ResponseTypes/StudentResponse.cs @@ -1,28 +1,11 @@ -using WinStudentGoalTracker.DataAccess; - namespace WinStudentGoalTracker.Models; public class StudentResponse { - public Guid IdStudent { get; set; } - public Guid? IdProgram { get; set; } + public Guid StudentId { get; set; } public string? Identifier { get; set; } - public int? ProgramYear { get; set; } - public DateTime? EnrollmentDate { get; set; } - public DateTime? ExpectedGrad { get; set; } - public DateTime? CreatedAt { get; set; } - - public static StudentResponse FromDatabaseModel(dbStudent student) - { - return new StudentResponse - { - IdStudent = student.IdStudent, - IdProgram = student.IdProgram, - Identifier = student.Identifier, - ProgramYear = student.ProgramYear, - EnrollmentDate = student.EnrollmentDate, - ExpectedGrad = student.ExpectedGrad, - CreatedAt = student.CreatedAt - }; - } + public DateTime? ExpectedGradDate { get; set; } + public DateTime? LastEntryDate { get; set; } + public int GoalCount { get; set; } + public int ProgressEventCount { get; set; } } diff --git a/db/Objects/views/v_student_card.sql b/db/Objects/views/v_student_card.sql new file mode 100644 index 0000000..5da95fa --- /dev/null +++ b/db/Objects/views/v_student_card.sql @@ -0,0 +1,17 @@ +CREATE OR REPLACE VIEW `v_student_card` AS +SELECT + s.`id_student` AS `studentId`, + s.`identifier` AS `identifier`, + s.`expected_grad` AS `expectedGradDate`, + MAX(pe.`created_at`) AS `lastEntryDate`, + COUNT(DISTINCT g.`id_goal`) AS `goalCount`, + COUNT(DISTINCT pe.`id_progress_event`) AS `progressEventCount` +FROM `student` s +LEFT JOIN `goal` g + ON g.`id_student` = s.`id_student` +LEFT JOIN `progress_event` pe + ON pe.`id_student` = s.`id_student` +GROUP BY + s.`id_student`, + s.`identifier`, + s.`expected_grad`; diff --git a/ui/winstudentgoaltracker/src/app/desktop/components/student-card/student-card.html b/ui/winstudentgoaltracker/src/app/desktop/components/student-card/student-card.html index d427c77..54158b8 100644 --- a/ui/winstudentgoaltracker/src/app/desktop/components/student-card/student-card.html +++ b/ui/winstudentgoaltracker/src/app/desktop/components/student-card/student-card.html @@ -2,7 +2,7 @@

🎓 {{ student().identifier }}

- Age: {{ student().age }} + Grad Date: {{ student().expectedGradDate }} @if (student().lastEntryDate) { Last entry: {{ student().lastEntryDate | date:'M/d/yy' }} diff --git a/ui/winstudentgoaltracker/src/app/mobile/components/student-card/student-card.html b/ui/winstudentgoaltracker/src/app/mobile/components/student-card/student-card.html index d5a96fa..6bc197e 100644 --- a/ui/winstudentgoaltracker/src/app/mobile/components/student-card/student-card.html +++ b/ui/winstudentgoaltracker/src/app/mobile/components/student-card/student-card.html @@ -1,6 +1,6 @@

{{ student().identifier }}

- Age: {{ student().age }} + Grad Date: {{ student().expectedGradDate }}
{{ student().goalCount }} goals {{ student().progressEventCount }} events diff --git a/ui/winstudentgoaltracker/src/app/mobile/pages/students/students.ts b/ui/winstudentgoaltracker/src/app/mobile/pages/students/students.ts index 664effc..31331a3 100644 --- a/ui/winstudentgoaltracker/src/app/mobile/pages/students/students.ts +++ b/ui/winstudentgoaltracker/src/app/mobile/pages/students/students.ts @@ -36,7 +36,7 @@ export class Students { // Loads the list of students assigned to the current user. // ***************************************************************** private loadStudents() { - this.studentService.getDummyStudentsForUser().then(data => { + this.studentService.getStudentsForUser().then(data => { if (!data.success) { diff --git a/ui/winstudentgoaltracker/src/app/shared/classes/student-card.dto.ts b/ui/winstudentgoaltracker/src/app/shared/classes/student-card.dto.ts index 5cec264..05784d1 100644 --- a/ui/winstudentgoaltracker/src/app/shared/classes/student-card.dto.ts +++ b/ui/winstudentgoaltracker/src/app/shared/classes/student-card.dto.ts @@ -1,8 +1,8 @@ export interface StudentCardDto { studentId: string; identifier: string; - age: number; - lastEntryDate: string | null; + expectedGradDate: Date; + lastEntryDate: Date | null; goalCount: number; progressEventCount: number; } diff --git a/ui/winstudentgoaltracker/src/app/shared/services/dummy-student.service.ts b/ui/winstudentgoaltracker/src/app/shared/services/dummy-student.service.ts index 685c5a7..2717d32 100644 --- a/ui/winstudentgoaltracker/src/app/shared/services/dummy-student.service.ts +++ b/ui/winstudentgoaltracker/src/app/shared/services/dummy-student.service.ts @@ -25,23 +25,23 @@ export class StudentService { { studentId: '1', identifier: 'J.B', - age: 21, - lastEntryDate: '2026-02-21', + expectedGradDate: new Date('2027-02-27'), + lastEntryDate: new Date('2026-02-21'), goalCount: 3, progressEventCount: 5, }, { studentId: '2', identifier: 'M.K', - age: 19, - lastEntryDate: '2026-02-25', + expectedGradDate: new Date('2027-02-27'), + lastEntryDate: new Date('2026-02-25'), goalCount: 4, progressEventCount: 8, }, { studentId: '3', identifier: 'A.R', - age: 22, + expectedGradDate: new Date('2027-02-27'), lastEntryDate: null, goalCount: 2, progressEventCount: 0, @@ -57,13 +57,13 @@ export class StudentService { // Returns students assigned to the current user with their // identifier, age, goal count, and progress event count. // ***************************************************************** - async getDummyStudentsForUser(): Promise> { + async getStudentsForUser(): Promise> { var payload = [ - { studentId: '1', identifier: 'J.B', age: 21, lastEntryDate: '2026-02-21', goalCount: 3, progressEventCount: 5 }, - { studentId: '2', identifier: 'M.K', age: 19, lastEntryDate: '2026-02-25', goalCount: 4, progressEventCount: 8 }, - { studentId: '3', identifier: 'A.R', age: 22, lastEntryDate: null, goalCount: 2, progressEventCount: 0 }, - { studentId: '4', identifier: 'T.W', age: 20, lastEntryDate: '2026-02-18', goalCount: 5, progressEventCount: 12 }, - { studentId: '5', identifier: 'L.C', age: 18, lastEntryDate: '2026-02-27', goalCount: 1, progressEventCount: 2 }, + { studentId: '1', identifier: 'J.B', expectedGradDate: new Date('2027-02-27'), lastEntryDate: new Date('2026-02-21'), goalCount: 3, progressEventCount: 5 }, + { studentId: '2', identifier: 'M.K', expectedGradDate: new Date('2027-02-27'), lastEntryDate: new Date('2026-02-25'), goalCount: 4, progressEventCount: 8 }, + { studentId: '3', identifier: 'A.R', expectedGradDate: new Date('2027-02-27'), lastEntryDate: null, goalCount: 2, progressEventCount: 0 }, + { studentId: '4', identifier: 'T.W', expectedGradDate: new Date('2027-02-27'), lastEntryDate: new Date('2026-02-18'), goalCount: 5, progressEventCount: 12 }, + { studentId: '5', identifier: 'L.C', expectedGradDate: new Date('2027-02-27'), lastEntryDate: new Date('2026-02-27'), goalCount: 1, progressEventCount: 2 }, ]; return ApiResult.ok(payload);