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 + } +
+ +
+