From b287276ec0f0633205354987969a2fdad68b6f43 Mon Sep 17 00:00:00 2001 From: ivan-pelly Date: Fri, 10 Apr 2026 15:31:56 -0700 Subject: [PATCH] Added persistent prompt to student progress report --- api/src/Controllers/ReportPromptController.cs | 191 ++++++++++++++++++ .../CreateReportPromptDto.cs | 8 + .../UpdateReportPromptDto.cs | 7 + .../Models/DatabaseObjects/dbReportPrompt.cs | 9 + .../Repositories/ReportPromptRepository.cs | 105 ++++++++++ .../ResponseTypes/ReportPromptResponse.cs | 9 + .../sp_ProgressReport_GetByStudentId.sql | 1 - .../procedures/sp_ReportPrompt_Delete.sql | 10 + .../procedures/sp_ReportPrompt_GetAll.sql | 12 ++ .../procedures/sp_ReportPrompt_GetById.sql | 15 ++ .../sp_ReportPrompt_GetByReportname.sql | 17 ++ .../procedures/sp_ReportPrompt_Insert.sql | 32 +++ .../procedures/sp_ReportPrompt_Update.sql | 15 ++ .../procedures/sp_Student_GetFullProfile.sql | 4 - .../sp_Student_GetWithAssignments.sql | 1 - db/Objects/tables/ReportPrompt.sql | 7 + .../student-progress-report.html | 16 ++ .../student-progress-report.scss | 31 +++ .../student-progress-report.ts | 62 +++++- .../app/shared/classes/report-prompt.dto.ts | 6 + .../shared/services/report-prompt.service.ts | 59 ++++++ 21 files changed, 606 insertions(+), 11 deletions(-) create mode 100644 api/src/Controllers/ReportPromptController.cs create mode 100644 api/src/DataAccess/Models/DataTransferObjects/CreateReportPromptDto.cs create mode 100644 api/src/DataAccess/Models/DataTransferObjects/UpdateReportPromptDto.cs create mode 100644 api/src/DataAccess/Models/DatabaseObjects/dbReportPrompt.cs create mode 100644 api/src/DataAccess/Repositories/ReportPromptRepository.cs create mode 100644 api/src/Models/ResponseTypes/ReportPromptResponse.cs create mode 100644 db/Objects/procedures/sp_ReportPrompt_Delete.sql create mode 100644 db/Objects/procedures/sp_ReportPrompt_GetAll.sql create mode 100644 db/Objects/procedures/sp_ReportPrompt_GetById.sql create mode 100644 db/Objects/procedures/sp_ReportPrompt_GetByReportname.sql create mode 100644 db/Objects/procedures/sp_ReportPrompt_Insert.sql create mode 100644 db/Objects/procedures/sp_ReportPrompt_Update.sql create mode 100644 db/Objects/tables/ReportPrompt.sql create mode 100644 ui/winstudentgoaltracker/src/app/shared/classes/report-prompt.dto.ts create mode 100644 ui/winstudentgoaltracker/src/app/shared/services/report-prompt.service.ts diff --git a/api/src/Controllers/ReportPromptController.cs b/api/src/Controllers/ReportPromptController.cs new file mode 100644 index 0000000..d272c51 --- /dev/null +++ b/api/src/Controllers/ReportPromptController.cs @@ -0,0 +1,191 @@ +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using WinStudentGoalTracker.Models; +using WinStudentGoalTracker.Models.ResponseTypes; +using WinStudentGoalTracker.BaseClasses; +using WinStudentGoalTracker.DataAccess; + +namespace WinStudentGoalTracker.Controllers; + +[ApiController] +[Route("api/[controller]")] +public class ReportPromptController : BaseController +{ + // ************************** Constructor ************************** + private readonly ReportPromptRepository _reportPromptRepository; + + public ReportPromptController() + { + _reportPromptRepository = new(); + } + + // ************************ Public Methods ************************* + + [HttpGet] + [Authorize(Roles = $"{UserRoles.Teacher},{UserRoles.ProgramAdmin},{UserRoles.SuperAdmin}")] + [ProducesResponseType(typeof(ResponseResult>), StatusCodes.Status200OK)] + public async Task>>> GetAll() + { + var (userId, email, programId, role, error) = GetProgramUserFromClaims(); + if (error is not null) + { + return error; + } + + var prompts = await _reportPromptRepository.GetAllAsync(); + + return Ok(new ResponseResult> + { + Success = true, + Message = "Report prompts retrieved successfully.", + Data = prompts + }); + } + + [HttpGet("{idReportPrompt:guid}")] + [Authorize(Roles = $"{UserRoles.Teacher},{UserRoles.ProgramAdmin},{UserRoles.SuperAdmin}")] + [ProducesResponseType(typeof(ResponseResult), StatusCodes.Status200OK)] + [ProducesResponseType(typeof(ResponseResult), StatusCodes.Status404NotFound)] + public async Task>> GetById(Guid idReportPrompt) + { + var (userId, email, programId, role, error) = GetProgramUserFromClaims(); + if (error is not null) + { + return error; + } + + var prompt = await _reportPromptRepository.GetByIdAsync(idReportPrompt); + if (prompt is null) + { + return NotFound(new ResponseResult + { + Success = false, + Message = "Report prompt not found." + }); + } + + return Ok(new ResponseResult + { + Success = true, + Message = "Report prompt retrieved successfully.", + Data = prompt + }); + } + + // ***************************************************************** + // Returns the report prompt for the given reportname scoped to + // the authenticated user's program. + // ***************************************************************** + [HttpGet("by-name/{reportname}")] + [Authorize(Roles = $"{UserRoles.Teacher},{UserRoles.ProgramAdmin},{UserRoles.SuperAdmin}")] + [ProducesResponseType(typeof(ResponseResult), StatusCodes.Status200OK)] + [ProducesResponseType(typeof(ResponseResult), StatusCodes.Status404NotFound)] + public async Task>> GetByReportname(string reportname) + { + var (userId, email, programId, role, error) = GetProgramUserFromClaims(); + if (error is not null) + { + return error; + } + + var prompt = await _reportPromptRepository.GetByReportnameAsync(reportname, programId); + if (prompt is null) + { + return NotFound(new ResponseResult + { + Success = false, + Message = "Report prompt not found." + }); + } + + return Ok(new ResponseResult + { + Success = true, + Message = "Report prompt retrieved successfully.", + Data = prompt + }); + } + + [HttpPost] + [Authorize(Roles = $"{UserRoles.Teacher},{UserRoles.ProgramAdmin},{UserRoles.SuperAdmin}")] + [ProducesResponseType(typeof(ResponseResult), StatusCodes.Status201Created)] + [ProducesResponseType(typeof(ResponseResult), StatusCodes.Status400BadRequest)] + public async Task>> Create([FromBody] CreateReportPromptDto dto) + { + var (userId, email, programId, role, error) = GetProgramUserFromClaims(); + if (error is not null) + { + return error; + } + + // Scope the new prompt to the authenticated user's program. + dto.ProgramId = programId.ToString(); + + var created = await _reportPromptRepository.InsertAsync(dto); + if (created is null) + { + return BadRequest(new ResponseResult + { + Success = false, + Message = "Unable to create report prompt." + }); + } + + return StatusCode(StatusCodes.Status201Created, new ResponseResult + { + Success = true, + Message = "Report prompt created successfully.", + Data = created + }); + } + + [HttpPut("{idReportPrompt:guid}")] + [Authorize(Roles = $"{UserRoles.Teacher},{UserRoles.ProgramAdmin},{UserRoles.SuperAdmin}")] + [ProducesResponseType(typeof(ResponseResult), StatusCodes.Status200OK)] + [ProducesResponseType(typeof(ResponseResult), StatusCodes.Status404NotFound)] + public async Task>> Update(Guid idReportPrompt, [FromBody] UpdateReportPromptDto dto) + { + var (userId, email, programId, role, error) = GetProgramUserFromClaims(); + if (error is not null) + { + return error; + } + + var updated = await _reportPromptRepository.UpdateAsync(idReportPrompt, dto); + + return Ok(new ResponseResult + { + Success = true, + Message = updated ? "Report prompt updated successfully." : "No changes were applied." + }); + } + + [HttpDelete("{idReportPrompt:guid}")] + [Authorize(Roles = $"{UserRoles.Teacher},{UserRoles.ProgramAdmin},{UserRoles.SuperAdmin}")] + [ProducesResponseType(typeof(ResponseResult), StatusCodes.Status200OK)] + [ProducesResponseType(typeof(ResponseResult), StatusCodes.Status404NotFound)] + public async Task>> Delete(Guid idReportPrompt) + { + var (userId, email, programId, role, error) = GetProgramUserFromClaims(); + if (error is not null) + { + return error; + } + + var deleted = await _reportPromptRepository.DeleteAsync(idReportPrompt); + if (!deleted) + { + return NotFound(new ResponseResult + { + Success = false, + Message = "Report prompt not found." + }); + } + + return Ok(new ResponseResult + { + Success = true, + Message = "Report prompt deleted." + }); + } +} diff --git a/api/src/DataAccess/Models/DataTransferObjects/CreateReportPromptDto.cs b/api/src/DataAccess/Models/DataTransferObjects/CreateReportPromptDto.cs new file mode 100644 index 0000000..c1e1245 --- /dev/null +++ b/api/src/DataAccess/Models/DataTransferObjects/CreateReportPromptDto.cs @@ -0,0 +1,8 @@ +namespace WinStudentGoalTracker.DataAccess; + +public class CreateReportPromptDto +{ + public string? ProgramId { get; set; } + public string? Prompt { get; set; } + public string? Reportname { get; set; } +} diff --git a/api/src/DataAccess/Models/DataTransferObjects/UpdateReportPromptDto.cs b/api/src/DataAccess/Models/DataTransferObjects/UpdateReportPromptDto.cs new file mode 100644 index 0000000..27d2401 --- /dev/null +++ b/api/src/DataAccess/Models/DataTransferObjects/UpdateReportPromptDto.cs @@ -0,0 +1,7 @@ +namespace WinStudentGoalTracker.DataAccess; + +public class UpdateReportPromptDto +{ + public string? Prompt { get; set; } + public string? Reportname { get; set; } +} diff --git a/api/src/DataAccess/Models/DatabaseObjects/dbReportPrompt.cs b/api/src/DataAccess/Models/DatabaseObjects/dbReportPrompt.cs new file mode 100644 index 0000000..7804c39 --- /dev/null +++ b/api/src/DataAccess/Models/DatabaseObjects/dbReportPrompt.cs @@ -0,0 +1,9 @@ +namespace WinStudentGoalTracker.DataAccess; + +public class dbReportPrompt +{ + public required Guid IdReportPrompt { get; set; } + public Guid? IdProgram { get; set; } + public string? Prompt { get; set; } + public string? Reportname { get; set; } +} diff --git a/api/src/DataAccess/Repositories/ReportPromptRepository.cs b/api/src/DataAccess/Repositories/ReportPromptRepository.cs new file mode 100644 index 0000000..8494c6e --- /dev/null +++ b/api/src/DataAccess/Repositories/ReportPromptRepository.cs @@ -0,0 +1,105 @@ +using System.Data; +using Dapper; +using MySql.Data.MySqlClient; +using WinStudentGoalTracker.Models; + +namespace WinStudentGoalTracker.DataAccess; + +public class ReportPromptRepository +{ + private IDbConnection Connection => new MySqlConnection(DatabaseManager.ConnectionString); + + // ***************************************************************** + // Returns all report prompts. + // ***************************************************************** + public async Task> GetAllAsync() + { + using var db = Connection; + return await db.QueryAsync( + "sp_ReportPrompt_GetAll", + commandType: CommandType.StoredProcedure); + } + + // ***************************************************************** + // Returns a single report prompt by its ID, or null if not found. + // ***************************************************************** + public async Task GetByIdAsync(Guid idReportPrompt) + { + using var db = Connection; + return await db.QuerySingleOrDefaultAsync( + "sp_ReportPrompt_GetById", + new { p_id_report_prompt = idReportPrompt.ToString() }, + commandType: CommandType.StoredProcedure); + } + + // ***************************************************************** + // Returns a single report prompt by its reportname and program, + // or null if not found. + // ***************************************************************** + public async Task GetByReportnameAsync(string reportname, Guid programId) + { + using var db = Connection; + return await db.QuerySingleOrDefaultAsync( + "sp_ReportPrompt_GetByReportname", + new + { + p_reportname = reportname, + p_id_program = programId.ToString() + }, + commandType: CommandType.StoredProcedure); + } + + // ***************************************************************** + // Inserts a new report prompt and returns the created record. + // ***************************************************************** + public async Task InsertAsync(CreateReportPromptDto dto) + { + var newId = Guid.NewGuid(); + using var db = Connection; + await db.ExecuteAsync( + "sp_ReportPrompt_Insert", + new + { + p_id_report_prompt = newId.ToString(), + p_id_program = dto.ProgramId, + p_prompt = dto.Prompt, + p_reportname = dto.Reportname + }, + commandType: CommandType.StoredProcedure); + + return await GetByIdAsync(newId); + } + + // ***************************************************************** + // Updates an existing report prompt. Returns true if a row was + // affected, false otherwise. + // ***************************************************************** + public async Task UpdateAsync(Guid idReportPrompt, UpdateReportPromptDto dto) + { + using var db = Connection; + var rowsAffected = await db.ExecuteScalarAsync( + "sp_ReportPrompt_Update", + new + { + p_id_report_prompt = idReportPrompt.ToString(), + p_prompt = dto.Prompt, + p_reportname = dto.Reportname + }, + commandType: CommandType.StoredProcedure); + return rowsAffected > 0; + } + + // ***************************************************************** + // Deletes a report prompt by its ID. Returns true if a row was + // affected, false otherwise. + // ***************************************************************** + public async Task DeleteAsync(Guid idReportPrompt) + { + using var db = Connection; + var rowsAffected = await db.ExecuteScalarAsync( + "sp_ReportPrompt_Delete", + new { p_id_report_prompt = idReportPrompt.ToString() }, + commandType: CommandType.StoredProcedure); + return rowsAffected > 0; + } +} diff --git a/api/src/Models/ResponseTypes/ReportPromptResponse.cs b/api/src/Models/ResponseTypes/ReportPromptResponse.cs new file mode 100644 index 0000000..0059fa0 --- /dev/null +++ b/api/src/Models/ResponseTypes/ReportPromptResponse.cs @@ -0,0 +1,9 @@ +namespace WinStudentGoalTracker.Models; + +public class ReportPromptResponse +{ + public Guid ReportPromptId { get; set; } + public Guid? ProgramId { get; set; } + public string? Prompt { get; set; } + public string? Reportname { get; set; } +} diff --git a/db/Objects/procedures/sp_ProgressReport_GetByStudentId.sql b/db/Objects/procedures/sp_ProgressReport_GetByStudentId.sql index cf060d9..007ec1c 100644 --- a/db/Objects/procedures/sp_ProgressReport_GetByStudentId.sql +++ b/db/Objects/procedures/sp_ProgressReport_GetByStudentId.sql @@ -21,7 +21,6 @@ BEGIN AND DATE(pe.`created_at`) <= p_to_date ) ORDER BY g.`category`; - -- Result set 2: Progress events within the date range, with benchmark names SELECT pe.`id_goal` AS `goalId`, diff --git a/db/Objects/procedures/sp_ReportPrompt_Delete.sql b/db/Objects/procedures/sp_ReportPrompt_Delete.sql new file mode 100644 index 0000000..49ca422 --- /dev/null +++ b/db/Objects/procedures/sp_ReportPrompt_Delete.sql @@ -0,0 +1,10 @@ +DELIMITER ;; +CREATE DEFINER=`root`@`%` PROCEDURE `sp_ReportPrompt_Delete`( + IN p_id_report_prompt CHAR(36) +) +BEGIN + DELETE FROM `ReportPrompt` + WHERE `id_report_prompt` = p_id_report_prompt; + SELECT ROW_COUNT() AS rowsAffected; +END;; +DELIMITER ; diff --git a/db/Objects/procedures/sp_ReportPrompt_GetAll.sql b/db/Objects/procedures/sp_ReportPrompt_GetAll.sql new file mode 100644 index 0000000..b0bdb7a --- /dev/null +++ b/db/Objects/procedures/sp_ReportPrompt_GetAll.sql @@ -0,0 +1,12 @@ +DELIMITER ;; +CREATE DEFINER=`root`@`%` PROCEDURE `sp_ReportPrompt_GetAll`() +BEGIN + SELECT + `id_report_prompt` AS `reportPromptId`, + `id_program` AS `programId`, + `prompt` AS `prompt`, + `reportname` AS `reportname` + FROM `ReportPrompt` + ORDER BY `reportname`; +END;; +DELIMITER ; diff --git a/db/Objects/procedures/sp_ReportPrompt_GetById.sql b/db/Objects/procedures/sp_ReportPrompt_GetById.sql new file mode 100644 index 0000000..41e0b4a --- /dev/null +++ b/db/Objects/procedures/sp_ReportPrompt_GetById.sql @@ -0,0 +1,15 @@ +DELIMITER ;; +CREATE DEFINER=`root`@`%` PROCEDURE `sp_ReportPrompt_GetById`( + IN p_id_report_prompt CHAR(36) +) +BEGIN + SELECT + `id_report_prompt` AS `reportPromptId`, + `id_program` AS `programId`, + `prompt` AS `prompt`, + `reportname` AS `reportname` + FROM `ReportPrompt` + WHERE `id_report_prompt` = p_id_report_prompt + LIMIT 1; +END;; +DELIMITER ; diff --git a/db/Objects/procedures/sp_ReportPrompt_GetByReportname.sql b/db/Objects/procedures/sp_ReportPrompt_GetByReportname.sql new file mode 100644 index 0000000..ad1e96e --- /dev/null +++ b/db/Objects/procedures/sp_ReportPrompt_GetByReportname.sql @@ -0,0 +1,17 @@ +DELIMITER ;; +CREATE DEFINER=`root`@`%` PROCEDURE `sp_ReportPrompt_GetByReportname`( + IN p_reportname CHAR(100), + IN p_id_program CHAR(36) +) +BEGIN + SELECT + `id_report_prompt` AS `reportPromptId`, + `id_program` AS `programId`, + `prompt` AS `prompt`, + `reportname` AS `reportname` + FROM `ReportPrompt` + WHERE `reportname` = p_reportname + AND `id_program` = p_id_program + LIMIT 1; +END;; +DELIMITER ; diff --git a/db/Objects/procedures/sp_ReportPrompt_Insert.sql b/db/Objects/procedures/sp_ReportPrompt_Insert.sql new file mode 100644 index 0000000..5b5bdcd --- /dev/null +++ b/db/Objects/procedures/sp_ReportPrompt_Insert.sql @@ -0,0 +1,32 @@ +DELIMITER ;; +CREATE DEFINER=`root`@`%` PROCEDURE `sp_ReportPrompt_Insert`( + IN p_id_report_prompt CHAR(36), + IN p_id_program CHAR(36), + IN p_prompt TEXT, + IN p_reportname CHAR(100) +) +BEGIN + INSERT INTO `ReportPrompt` + ( + `id_report_prompt`, + `id_program`, + `prompt`, + `reportname` + ) + VALUES + ( + p_id_report_prompt, + p_id_program, + p_prompt, + p_reportname + ); + SELECT + `id_report_prompt` AS `reportPromptId`, + `id_program` AS `programId`, + `prompt` AS `prompt`, + `reportname` AS `reportname` + FROM `ReportPrompt` + WHERE `id_report_prompt` = p_id_report_prompt + LIMIT 1; +END;; +DELIMITER ; diff --git a/db/Objects/procedures/sp_ReportPrompt_Update.sql b/db/Objects/procedures/sp_ReportPrompt_Update.sql new file mode 100644 index 0000000..bedcbb5 --- /dev/null +++ b/db/Objects/procedures/sp_ReportPrompt_Update.sql @@ -0,0 +1,15 @@ +DELIMITER ;; +CREATE DEFINER=`root`@`%` PROCEDURE `sp_ReportPrompt_Update`( + IN p_id_report_prompt CHAR(36), + IN p_prompt TEXT, + IN p_reportname CHAR(100) +) +BEGIN + UPDATE `ReportPrompt` + SET + `prompt` = p_prompt, + `reportname` = p_reportname + WHERE `id_report_prompt` = p_id_report_prompt; + SELECT ROW_COUNT() AS rowsAffected; +END;; +DELIMITER ; diff --git a/db/Objects/procedures/sp_Student_GetFullProfile.sql b/db/Objects/procedures/sp_Student_GetFullProfile.sql index 7eea52f..80ff8c2 100644 --- a/db/Objects/procedures/sp_Student_GetFullProfile.sql +++ b/db/Objects/procedures/sp_Student_GetFullProfile.sql @@ -14,7 +14,6 @@ BEGIN FROM v_student_card WHERE studentId = p_id_student LIMIT 1; - -- Result set 2: Goals SELECT s.`identifier` AS `studentIdentifier`, @@ -33,7 +32,6 @@ BEGIN 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`, @@ -51,7 +49,6 @@ BEGIN 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`, @@ -62,7 +59,6 @@ BEGIN 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`, diff --git a/db/Objects/procedures/sp_Student_GetWithAssignments.sql b/db/Objects/procedures/sp_Student_GetWithAssignments.sql index 33d4d8d..481697d 100644 --- a/db/Objects/procedures/sp_Student_GetWithAssignments.sql +++ b/db/Objects/procedures/sp_Student_GetWithAssignments.sql @@ -18,7 +18,6 @@ BEGIN INNER JOIN student s ON s.id_student = vc.studentId WHERE s.id_program = p_id_program ORDER BY vc.studentId; - IF p_scope = 'all' THEN SELECT us.id_user_student, diff --git a/db/Objects/tables/ReportPrompt.sql b/db/Objects/tables/ReportPrompt.sql new file mode 100644 index 0000000..2b8ce7e --- /dev/null +++ b/db/Objects/tables/ReportPrompt.sql @@ -0,0 +1,7 @@ +CREATE TABLE `ReportPrompt` ( + `id_ReportPrompt` char(36) NOT NULL DEFAULT (uuid()), + `prompt` text NOT NULL, + `reportname` char(100) NOT NULL, + `id_program` char(36) DEFAULT 'NULL', + PRIMARY KEY (`id_ReportPrompt`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; diff --git a/ui/winstudentgoaltracker/src/app/desktop/components/student-progress-report/student-progress-report.html b/ui/winstudentgoaltracker/src/app/desktop/components/student-progress-report/student-progress-report.html index 9f0deea..2e09072 100644 --- a/ui/winstudentgoaltracker/src/app/desktop/components/student-progress-report/student-progress-report.html +++ b/ui/winstudentgoaltracker/src/app/desktop/components/student-progress-report/student-progress-report.html @@ -45,6 +45,22 @@ } +
+
+ + @if (promptSaved()) { + ✓ Saved + } +
+ +
+