mirror of
https://github.com/opelly27/WinStudentGoalTracker.git
synced 2026-05-20 01:47:41 +00:00
Added persistent prompt to student progress report
This commit is contained in:
@@ -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<IEnumerable<ReportPromptResponse>>), StatusCodes.Status200OK)]
|
||||||
|
public async Task<ActionResult<ResponseResult<IEnumerable<ReportPromptResponse>>>> GetAll()
|
||||||
|
{
|
||||||
|
var (userId, email, programId, role, error) = GetProgramUserFromClaims();
|
||||||
|
if (error is not null)
|
||||||
|
{
|
||||||
|
return error;
|
||||||
|
}
|
||||||
|
|
||||||
|
var prompts = await _reportPromptRepository.GetAllAsync();
|
||||||
|
|
||||||
|
return Ok(new ResponseResult<IEnumerable<ReportPromptResponse>>
|
||||||
|
{
|
||||||
|
Success = true,
|
||||||
|
Message = "Report prompts retrieved successfully.",
|
||||||
|
Data = prompts
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet("{idReportPrompt:guid}")]
|
||||||
|
[Authorize(Roles = $"{UserRoles.Teacher},{UserRoles.ProgramAdmin},{UserRoles.SuperAdmin}")]
|
||||||
|
[ProducesResponseType(typeof(ResponseResult<ReportPromptResponse>), StatusCodes.Status200OK)]
|
||||||
|
[ProducesResponseType(typeof(ResponseResult<ReportPromptResponse>), StatusCodes.Status404NotFound)]
|
||||||
|
public async Task<ActionResult<ResponseResult<ReportPromptResponse>>> 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<ReportPromptResponse>
|
||||||
|
{
|
||||||
|
Success = false,
|
||||||
|
Message = "Report prompt not found."
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return Ok(new ResponseResult<ReportPromptResponse>
|
||||||
|
{
|
||||||
|
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<ReportPromptResponse>), StatusCodes.Status200OK)]
|
||||||
|
[ProducesResponseType(typeof(ResponseResult<ReportPromptResponse>), StatusCodes.Status404NotFound)]
|
||||||
|
public async Task<ActionResult<ResponseResult<ReportPromptResponse>>> 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<ReportPromptResponse>
|
||||||
|
{
|
||||||
|
Success = false,
|
||||||
|
Message = "Report prompt not found."
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return Ok(new ResponseResult<ReportPromptResponse>
|
||||||
|
{
|
||||||
|
Success = true,
|
||||||
|
Message = "Report prompt retrieved successfully.",
|
||||||
|
Data = prompt
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPost]
|
||||||
|
[Authorize(Roles = $"{UserRoles.Teacher},{UserRoles.ProgramAdmin},{UserRoles.SuperAdmin}")]
|
||||||
|
[ProducesResponseType(typeof(ResponseResult<ReportPromptResponse>), StatusCodes.Status201Created)]
|
||||||
|
[ProducesResponseType(typeof(ResponseResult<ReportPromptResponse>), StatusCodes.Status400BadRequest)]
|
||||||
|
public async Task<ActionResult<ResponseResult<ReportPromptResponse>>> 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<ReportPromptResponse>
|
||||||
|
{
|
||||||
|
Success = false,
|
||||||
|
Message = "Unable to create report prompt."
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return StatusCode(StatusCodes.Status201Created, new ResponseResult<ReportPromptResponse>
|
||||||
|
{
|
||||||
|
Success = true,
|
||||||
|
Message = "Report prompt created successfully.",
|
||||||
|
Data = created
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPut("{idReportPrompt:guid}")]
|
||||||
|
[Authorize(Roles = $"{UserRoles.Teacher},{UserRoles.ProgramAdmin},{UserRoles.SuperAdmin}")]
|
||||||
|
[ProducesResponseType(typeof(ResponseResult<object>), StatusCodes.Status200OK)]
|
||||||
|
[ProducesResponseType(typeof(ResponseResult<object>), StatusCodes.Status404NotFound)]
|
||||||
|
public async Task<ActionResult<ResponseResult<object>>> 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<object>
|
||||||
|
{
|
||||||
|
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<object>), StatusCodes.Status200OK)]
|
||||||
|
[ProducesResponseType(typeof(ResponseResult<object>), StatusCodes.Status404NotFound)]
|
||||||
|
public async Task<ActionResult<ResponseResult<object>>> 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<object>
|
||||||
|
{
|
||||||
|
Success = false,
|
||||||
|
Message = "Report prompt not found."
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return Ok(new ResponseResult<object>
|
||||||
|
{
|
||||||
|
Success = true,
|
||||||
|
Message = "Report prompt deleted."
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
namespace WinStudentGoalTracker.DataAccess;
|
||||||
|
|
||||||
|
public class UpdateReportPromptDto
|
||||||
|
{
|
||||||
|
public string? Prompt { get; set; }
|
||||||
|
public string? Reportname { get; set; }
|
||||||
|
}
|
||||||
@@ -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; }
|
||||||
|
}
|
||||||
@@ -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<IEnumerable<ReportPromptResponse>> GetAllAsync()
|
||||||
|
{
|
||||||
|
using var db = Connection;
|
||||||
|
return await db.QueryAsync<ReportPromptResponse>(
|
||||||
|
"sp_ReportPrompt_GetAll",
|
||||||
|
commandType: CommandType.StoredProcedure);
|
||||||
|
}
|
||||||
|
|
||||||
|
// *****************************************************************
|
||||||
|
// Returns a single report prompt by its ID, or null if not found.
|
||||||
|
// *****************************************************************
|
||||||
|
public async Task<ReportPromptResponse?> GetByIdAsync(Guid idReportPrompt)
|
||||||
|
{
|
||||||
|
using var db = Connection;
|
||||||
|
return await db.QuerySingleOrDefaultAsync<ReportPromptResponse>(
|
||||||
|
"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<ReportPromptResponse?> GetByReportnameAsync(string reportname, Guid programId)
|
||||||
|
{
|
||||||
|
using var db = Connection;
|
||||||
|
return await db.QuerySingleOrDefaultAsync<ReportPromptResponse>(
|
||||||
|
"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<ReportPromptResponse?> 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<bool> UpdateAsync(Guid idReportPrompt, UpdateReportPromptDto dto)
|
||||||
|
{
|
||||||
|
using var db = Connection;
|
||||||
|
var rowsAffected = await db.ExecuteScalarAsync<int>(
|
||||||
|
"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<bool> DeleteAsync(Guid idReportPrompt)
|
||||||
|
{
|
||||||
|
using var db = Connection;
|
||||||
|
var rowsAffected = await db.ExecuteScalarAsync<int>(
|
||||||
|
"sp_ReportPrompt_Delete",
|
||||||
|
new { p_id_report_prompt = idReportPrompt.ToString() },
|
||||||
|
commandType: CommandType.StoredProcedure);
|
||||||
|
return rowsAffected > 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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; }
|
||||||
|
}
|
||||||
@@ -21,7 +21,6 @@ BEGIN
|
|||||||
AND DATE(pe.`created_at`) <= p_to_date
|
AND DATE(pe.`created_at`) <= p_to_date
|
||||||
)
|
)
|
||||||
ORDER BY g.`category`;
|
ORDER BY g.`category`;
|
||||||
|
|
||||||
-- Result set 2: Progress events within the date range, with benchmark names
|
-- Result set 2: Progress events within the date range, with benchmark names
|
||||||
SELECT
|
SELECT
|
||||||
pe.`id_goal` AS `goalId`,
|
pe.`id_goal` AS `goalId`,
|
||||||
|
|||||||
@@ -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 ;
|
||||||
@@ -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 ;
|
||||||
@@ -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 ;
|
||||||
@@ -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 ;
|
||||||
@@ -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 ;
|
||||||
@@ -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 ;
|
||||||
@@ -14,7 +14,6 @@ BEGIN
|
|||||||
FROM v_student_card
|
FROM v_student_card
|
||||||
WHERE studentId = p_id_student
|
WHERE studentId = p_id_student
|
||||||
LIMIT 1;
|
LIMIT 1;
|
||||||
|
|
||||||
-- Result set 2: Goals
|
-- Result set 2: Goals
|
||||||
SELECT
|
SELECT
|
||||||
s.`identifier` AS `studentIdentifier`,
|
s.`identifier` AS `studentIdentifier`,
|
||||||
@@ -33,7 +32,6 @@ BEGIN
|
|||||||
INNER JOIN `student` s ON s.`id_student` = vc.`studentId`
|
INNER JOIN `student` s ON s.`id_student` = vc.`studentId`
|
||||||
WHERE vc.`studentId` = p_id_student
|
WHERE vc.`studentId` = p_id_student
|
||||||
ORDER BY vc.`goalId`;
|
ORDER BY vc.`goalId`;
|
||||||
|
|
||||||
-- Result set 3: Benchmarks
|
-- Result set 3: Benchmarks
|
||||||
SELECT
|
SELECT
|
||||||
s.`identifier` AS `studentIdentifier`,
|
s.`identifier` AS `studentIdentifier`,
|
||||||
@@ -51,7 +49,6 @@ BEGIN
|
|||||||
LEFT JOIN `user` u ON u.`id_user` = b.`id_user_created`
|
LEFT JOIN `user` u ON u.`id_user` = b.`id_user_created`
|
||||||
WHERE g.`id_student` = p_id_student
|
WHERE g.`id_student` = p_id_student
|
||||||
ORDER BY b.`created_at` DESC;
|
ORDER BY b.`created_at` DESC;
|
||||||
|
|
||||||
-- Result set 4: Progress events (all goals for this student)
|
-- Result set 4: Progress events (all goals for this student)
|
||||||
SELECT
|
SELECT
|
||||||
vc.`progressEventId`,
|
vc.`progressEventId`,
|
||||||
@@ -62,7 +59,6 @@ BEGIN
|
|||||||
FROM `v_progress_event_card` vc
|
FROM `v_progress_event_card` vc
|
||||||
WHERE vc.`studentId` = p_id_student
|
WHERE vc.`studentId` = p_id_student
|
||||||
ORDER BY vc.`createdAt` DESC;
|
ORDER BY vc.`createdAt` DESC;
|
||||||
|
|
||||||
-- Result set 5: Benchmark/progress-event associations
|
-- Result set 5: Benchmark/progress-event associations
|
||||||
SELECT
|
SELECT
|
||||||
peb.`id_progress_event` AS `progressEventId`,
|
peb.`id_progress_event` AS `progressEventId`,
|
||||||
|
|||||||
@@ -18,7 +18,6 @@ BEGIN
|
|||||||
INNER JOIN student s ON s.id_student = vc.studentId
|
INNER JOIN student s ON s.id_student = vc.studentId
|
||||||
WHERE s.id_program = p_id_program
|
WHERE s.id_program = p_id_program
|
||||||
ORDER BY vc.studentId;
|
ORDER BY vc.studentId;
|
||||||
|
|
||||||
IF p_scope = 'all' THEN
|
IF p_scope = 'all' THEN
|
||||||
SELECT
|
SELECT
|
||||||
us.id_user_student,
|
us.id_user_student,
|
||||||
|
|||||||
@@ -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;
|
||||||
+16
@@ -45,6 +45,22 @@
|
|||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
<div class="field">
|
||||||
|
<div class="field-label-row">
|
||||||
|
<label class="field-label" for="prompt">Prompt</label>
|
||||||
|
@if (promptSaved()) {
|
||||||
|
<span class="save-indicator">✓ Saved</span>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
<textarea
|
||||||
|
id="prompt"
|
||||||
|
class="field-input prompt-textarea"
|
||||||
|
rows="6"
|
||||||
|
[(ngModel)]="promptText"
|
||||||
|
(ngModelChange)="onPromptChange()">
|
||||||
|
</textarea>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="actions">
|
<div class="actions">
|
||||||
<button
|
<button
|
||||||
class="toolbar-btn run-btn"
|
class="toolbar-btn run-btn"
|
||||||
|
|||||||
+31
@@ -55,6 +55,37 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.field-label-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
margin-bottom: 6px;
|
||||||
|
|
||||||
|
.field-label {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.save-indicator {
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: var(--accent-green, #22c55e);
|
||||||
|
animation: fadeIn 0.2s ease-in;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes fadeIn {
|
||||||
|
from { opacity: 0; }
|
||||||
|
to { opacity: 1; }
|
||||||
|
}
|
||||||
|
|
||||||
|
.prompt-textarea {
|
||||||
|
width: 100%;
|
||||||
|
resize: vertical;
|
||||||
|
font-family: inherit;
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
.run-btn {
|
.run-btn {
|
||||||
background: var(--accent-indigo) !important;
|
background: var(--accent-indigo) !important;
|
||||||
color: #fff !important;
|
color: #fff !important;
|
||||||
|
|||||||
+57
-5
@@ -1,9 +1,9 @@
|
|||||||
import { Component, inject, signal } from '@angular/core';
|
import { Component, inject, signal, OnDestroy } from '@angular/core';
|
||||||
import { FormsModule } from '@angular/forms';
|
import { FormsModule } from '@angular/forms';
|
||||||
import { Router } from '@angular/router';
|
import { Router } from '@angular/router';
|
||||||
import { StudentService } from '../../../shared/services/student.service';
|
import { StudentService } from '../../../shared/services/student.service';
|
||||||
|
import { ReportPromptService } from '../../../shared/services/report-prompt.service';
|
||||||
import { StudentCardDto } from '../../../shared/classes/student-card.dto';
|
import { StudentCardDto } from '../../../shared/classes/student-card.dto';
|
||||||
import { StudentGoalItem } from '../../../shared/classes/student-goal';
|
|
||||||
import { toIsoDateString } from '../../../shared/utils/format-date';
|
import { toIsoDateString } from '../../../shared/utils/format-date';
|
||||||
|
|
||||||
interface GoalCheckItem {
|
interface GoalCheckItem {
|
||||||
@@ -18,29 +18,41 @@ interface GoalCheckItem {
|
|||||||
templateUrl: './student-progress-report.html',
|
templateUrl: './student-progress-report.html',
|
||||||
styleUrl: './student-progress-report.scss',
|
styleUrl: './student-progress-report.scss',
|
||||||
})
|
})
|
||||||
export class StudentProgressReport {
|
export class StudentProgressReport implements OnDestroy {
|
||||||
|
|
||||||
// ************************** Constructor **************************
|
// ************************** Constructor **************************
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
this.loadStudents();
|
this.loadStudents();
|
||||||
|
this.loadPrompt();
|
||||||
}
|
}
|
||||||
|
|
||||||
// ************************** Declarations *************************
|
// ************************** Declarations *************************
|
||||||
|
|
||||||
private readonly router = inject(Router);
|
private readonly router = inject(Router);
|
||||||
private readonly studentService = inject(StudentService);
|
private readonly studentService = inject(StudentService);
|
||||||
|
private readonly reportPromptService = inject(ReportPromptService);
|
||||||
protected readonly students = signal<StudentCardDto[]>([]);
|
protected readonly students = signal<StudentCardDto[]>([]);
|
||||||
protected readonly goalItems = signal<GoalCheckItem[]>([]);
|
protected readonly goalItems = signal<GoalCheckItem[]>([]);
|
||||||
protected readonly running = signal(false);
|
protected readonly running = signal(false);
|
||||||
|
protected readonly promptSaved = signal(false);
|
||||||
protected selectedStudentId = '';
|
protected selectedStudentId = '';
|
||||||
protected fromDate = '';
|
protected fromDate = '';
|
||||||
protected toDate = '';
|
protected toDate = '';
|
||||||
|
protected promptText = '';
|
||||||
|
private promptId = '';
|
||||||
|
private debounceTimer: ReturnType<typeof setTimeout> | null = null;
|
||||||
|
private savedTimer: ReturnType<typeof setTimeout> | null = null;
|
||||||
|
|
||||||
// ************************** Properties ***************************
|
// ************************** Properties ***************************
|
||||||
|
|
||||||
// ************************ Public Methods *************************
|
// ************************ Public Methods *************************
|
||||||
|
|
||||||
|
ngOnDestroy() {
|
||||||
|
if (this.debounceTimer) clearTimeout(this.debounceTimer);
|
||||||
|
if (this.savedTimer) clearTimeout(this.savedTimer);
|
||||||
|
}
|
||||||
|
|
||||||
// ************************ Event Handlers *************************
|
// ************************ Event Handlers *************************
|
||||||
|
|
||||||
onBack() {
|
onBack() {
|
||||||
@@ -88,6 +100,26 @@ export class StudentProgressReport {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// *****************************************************************
|
||||||
|
// Debounces prompt changes and auto-saves after 1 second of
|
||||||
|
// inactivity. Shows a brief "Saved" indicator on success.
|
||||||
|
// *****************************************************************
|
||||||
|
onPromptChange() {
|
||||||
|
this.promptSaved.set(false);
|
||||||
|
if (this.debounceTimer) clearTimeout(this.debounceTimer);
|
||||||
|
|
||||||
|
if (!this.promptId) return;
|
||||||
|
|
||||||
|
this.debounceTimer = setTimeout(async () => {
|
||||||
|
const result = await this.reportPromptService.updatePrompt(this.promptId, this.promptText);
|
||||||
|
if (result.success) {
|
||||||
|
this.promptSaved.set(true);
|
||||||
|
if (this.savedTimer) clearTimeout(this.savedTimer);
|
||||||
|
this.savedTimer = setTimeout(() => this.promptSaved.set(false), 3000);
|
||||||
|
}
|
||||||
|
}, 1000);
|
||||||
|
}
|
||||||
|
|
||||||
// *****************************************************************
|
// *****************************************************************
|
||||||
// Calls the API to generate the markdown report, passing only
|
// Calls the API to generate the markdown report, passing only
|
||||||
// the checked goal IDs, and triggers a browser download.
|
// the checked goal IDs, and triggers a browser download.
|
||||||
@@ -129,14 +161,34 @@ export class StudentProgressReport {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// *****************************************************************
|
// *****************************************************************
|
||||||
// Triggers a browser download of the given markdown content.
|
// Loads the prompt for 'progressreport' from the API.
|
||||||
|
// *****************************************************************
|
||||||
|
private async loadPrompt() {
|
||||||
|
const result = await this.reportPromptService.getByReportname('progressreport');
|
||||||
|
if (result.success && result.payload) {
|
||||||
|
this.promptId = result.payload.reportPromptId;
|
||||||
|
this.promptText = result.payload.prompt;
|
||||||
|
} else {
|
||||||
|
console.error('[loadPrompt] Failed to load prompt:', result.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// *****************************************************************
|
||||||
|
// Triggers a browser download of the given markdown content,
|
||||||
|
// prepending the prompt text at the top of the file.
|
||||||
// *****************************************************************
|
// *****************************************************************
|
||||||
private downloadMarkdown(content: string) {
|
private downloadMarkdown(content: string) {
|
||||||
const student = this.students().find(s => s.studentId === this.selectedStudentId);
|
const student = this.students().find(s => s.studentId === this.selectedStudentId);
|
||||||
const name = student ? student.identifier.replace(/\s+/g, '_') : 'report';
|
const name = student ? student.identifier.replace(/\s+/g, '_') : 'report';
|
||||||
const filename = `${name}_progress_report_${this.fromDate}_to_${this.toDate}.md`;
|
const filename = `${name}_progress_report_${this.fromDate}_to_${this.toDate}.md`;
|
||||||
|
|
||||||
const blob = new Blob([content], { type: 'text/markdown;charset=utf-8' });
|
// Prepend the prompt if one exists.
|
||||||
|
let output = content;
|
||||||
|
if (this.promptText.trim()) {
|
||||||
|
output = this.promptText.trim() + '\n\n---\n\n' + content;
|
||||||
|
}
|
||||||
|
|
||||||
|
const blob = new Blob([output], { type: 'text/markdown;charset=utf-8' });
|
||||||
const url = URL.createObjectURL(blob);
|
const url = URL.createObjectURL(blob);
|
||||||
const a = document.createElement('a');
|
const a = document.createElement('a');
|
||||||
a.href = url;
|
a.href = url;
|
||||||
|
|||||||
@@ -0,0 +1,6 @@
|
|||||||
|
export interface ReportPromptDto {
|
||||||
|
reportPromptId: string;
|
||||||
|
programId: string;
|
||||||
|
prompt: string;
|
||||||
|
reportname: string;
|
||||||
|
}
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
import { HttpClient, HttpErrorResponse } from '@angular/common/http';
|
||||||
|
import { inject, Injectable } from '@angular/core';
|
||||||
|
import { firstValueFrom } from 'rxjs';
|
||||||
|
import { environment } from '../../../environments/environment';
|
||||||
|
import { ApiResult } from '../classes/api-result';
|
||||||
|
import { ResponseResult } from '../classes/auth.models';
|
||||||
|
import { ReportPromptDto } from '../classes/report-prompt.dto';
|
||||||
|
import { describeHttpError } from '../classes/http-errors';
|
||||||
|
|
||||||
|
@Injectable({
|
||||||
|
providedIn: 'root',
|
||||||
|
})
|
||||||
|
export class ReportPromptService {
|
||||||
|
|
||||||
|
// ************************** Declarations *************************
|
||||||
|
|
||||||
|
private readonly http = inject(HttpClient);
|
||||||
|
private readonly base = environment.apiBaseUrl;
|
||||||
|
|
||||||
|
// ************************ Public Methods *************************
|
||||||
|
|
||||||
|
// *****************************************************************
|
||||||
|
// Returns the report prompt for the given reportname, scoped to
|
||||||
|
// the authenticated user's program.
|
||||||
|
// *****************************************************************
|
||||||
|
async getByReportname(name: string): Promise<ApiResult<ReportPromptDto>> {
|
||||||
|
try {
|
||||||
|
const result = await firstValueFrom(
|
||||||
|
this.http.get<ResponseResult<ReportPromptDto>>(
|
||||||
|
`${this.base}/api/ReportPrompt/by-name/${encodeURIComponent(name)}`
|
||||||
|
)
|
||||||
|
);
|
||||||
|
return result.success && result.data
|
||||||
|
? ApiResult.ok(result.data)
|
||||||
|
: ApiResult.fail(result.message);
|
||||||
|
} catch (error) {
|
||||||
|
return ApiResult.fail(describeHttpError(error as HttpErrorResponse));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// *****************************************************************
|
||||||
|
// Updates the prompt text for an existing report prompt.
|
||||||
|
// *****************************************************************
|
||||||
|
async updatePrompt(id: string, prompt: string): Promise<ApiResult> {
|
||||||
|
try {
|
||||||
|
const result = await firstValueFrom(
|
||||||
|
this.http.put<ResponseResult<void>>(
|
||||||
|
`${this.base}/api/ReportPrompt/${id}`,
|
||||||
|
{ prompt }
|
||||||
|
)
|
||||||
|
);
|
||||||
|
return result.success
|
||||||
|
? ApiResult.empty()
|
||||||
|
: ApiResult.fail(result.message);
|
||||||
|
} catch (error) {
|
||||||
|
return ApiResult.fail(describeHttpError(error as HttpErrorResponse));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user