203 lines
8.1 KiB
C#
203 lines
8.1 KiB
C#
using Microsoft.AspNetCore.Authorization;
|
|
using Microsoft.AspNetCore.Mvc;
|
|
|
|
|
|
/// <summary>
|
|
/// Clinical alert listing, acknowledgment, and resolution.
|
|
/// </summary>
|
|
[ApiController]
|
|
[Produces("application/json")]
|
|
[Authorize]
|
|
public class AlertsController : ControllerBase
|
|
{
|
|
private readonly IAlertService _alerts;
|
|
|
|
public AlertsController(IAlertService alerts) => _alerts = alerts;
|
|
|
|
/// <summary>
|
|
/// Lists alerts for a single encounter with optional status filter.
|
|
/// </summary>
|
|
/// <param name="encounterId">Encounter id.</param>
|
|
/// <param name="status">Optional status filter (DB literal, e.g. OPEN).</param>
|
|
/// <param name="page">Page number (1-based).</param>
|
|
/// <param name="pageSize">Results per page.</param>
|
|
/// <returns>A paginated list of alerts for the encounter.</returns>
|
|
[HttpGet("api/v1/encounters/{encounterId:guid}/alerts")]
|
|
[AuthorizePermission(ClinicalPermissions.AlertsRead)]
|
|
[ProducesResponseType(typeof(ApiResponse<object>), StatusCodes.Status200OK)]
|
|
[ProducesResponseType(typeof(ApiResponse<object>), StatusCodes.Status400BadRequest)]
|
|
public async Task<IActionResult> ListByEncounter(
|
|
Guid encounterId,
|
|
[FromQuery] string? status,
|
|
[FromQuery] int page = 1,
|
|
[FromQuery] int pageSize = 20)
|
|
{
|
|
AlertStatus? parsedStatus = null;
|
|
if (!string.IsNullOrEmpty(status))
|
|
{
|
|
try
|
|
{
|
|
parsedStatus = AlertStatusExtensions.FromDbString(status);
|
|
}
|
|
catch (ArgumentOutOfRangeException)
|
|
{
|
|
return BadRequest(ApiResponse<object>.Fail(400, "Invalid status filter.", "INVALID_STATUS"));
|
|
}
|
|
}
|
|
|
|
var result = await _alerts.ListByEncounterAsync(encounterId, parsedStatus, page, pageSize);
|
|
return Ok(ApiResponse<object>.Ok(new
|
|
{
|
|
items = result.Items,
|
|
page = result.Page,
|
|
pageSize = result.PageSize,
|
|
totalCount = result.TotalCount,
|
|
totalPages = result.TotalPages
|
|
}));
|
|
}
|
|
|
|
/// <summary>
|
|
/// Lists alerts across all encounters with optional status, severity, and department filters.
|
|
/// </summary>
|
|
/// <param name="status">Optional status filter (DB literal, e.g. OPEN).</param>
|
|
/// <param name="severity">Optional severity filter (DB literal, e.g. CRITICAL).</param>
|
|
/// <param name="department">Optional department filter.</param>
|
|
/// <param name="page">Page number (1-based).</param>
|
|
/// <param name="pageSize">Results per page.</param>
|
|
/// <returns>A paginated list of alerts.</returns>
|
|
[HttpGet("api/v1/alerts")]
|
|
[AuthorizePermission(ClinicalPermissions.AlertsRead)]
|
|
[ProducesResponseType(typeof(ApiResponse<object>), StatusCodes.Status200OK)]
|
|
[ProducesResponseType(typeof(ApiResponse<object>), StatusCodes.Status400BadRequest)]
|
|
public async Task<IActionResult> ListGlobal(
|
|
[FromQuery] string? status,
|
|
[FromQuery] string? severity,
|
|
[FromQuery] string? department,
|
|
[FromQuery] int page = 1,
|
|
[FromQuery] int pageSize = 20)
|
|
{
|
|
AlertStatus? parsedStatus = null;
|
|
if (!string.IsNullOrEmpty(status))
|
|
{
|
|
try
|
|
{
|
|
parsedStatus = AlertStatusExtensions.FromDbString(status);
|
|
}
|
|
catch (ArgumentOutOfRangeException)
|
|
{
|
|
return BadRequest(ApiResponse<object>.Fail(400, "Invalid status filter.", "INVALID_STATUS"));
|
|
}
|
|
}
|
|
|
|
AlertSeverity? parsedSeverity = null;
|
|
if (!string.IsNullOrEmpty(severity))
|
|
{
|
|
try
|
|
{
|
|
parsedSeverity = AlertSeverityExtensions.FromDbString(severity);
|
|
}
|
|
catch (ArgumentOutOfRangeException)
|
|
{
|
|
return BadRequest(ApiResponse<object>.Fail(400, "Invalid severity filter.", "INVALID_SEVERITY"));
|
|
}
|
|
}
|
|
|
|
Department? parsedDepartment = null;
|
|
if (!string.IsNullOrEmpty(department))
|
|
{
|
|
try
|
|
{
|
|
parsedDepartment = DepartmentExtensions.FromDbString(department);
|
|
}
|
|
catch (ArgumentOutOfRangeException)
|
|
{
|
|
return BadRequest(ApiResponse<object>.Fail(400, "Invalid department filter.", "INVALID_DEPARTMENT"));
|
|
}
|
|
}
|
|
|
|
var result = await _alerts.ListGlobalAsync(parsedStatus, parsedSeverity, parsedDepartment, page, pageSize);
|
|
return Ok(ApiResponse<object>.Ok(new
|
|
{
|
|
items = result.Items,
|
|
page = result.Page,
|
|
pageSize = result.PageSize,
|
|
totalCount = result.TotalCount,
|
|
totalPages = result.TotalPages
|
|
}));
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets a single alert by id, including its encounter.
|
|
/// </summary>
|
|
/// <param name="id">Alert id.</param>
|
|
/// <returns>The alert record.</returns>
|
|
[HttpGet("api/v1/alerts/{id:guid}")]
|
|
[AuthorizePermission(ClinicalPermissions.AlertsRead)]
|
|
[ProducesResponseType(typeof(ApiResponse<ClinicalAlert>), StatusCodes.Status200OK)]
|
|
[ProducesResponseType(typeof(ApiResponse<object>), StatusCodes.Status404NotFound)]
|
|
public async Task<IActionResult> Get(Guid id)
|
|
{
|
|
var alert = await _alerts.GetByIdAsync(id);
|
|
return Ok(ApiResponse<ClinicalAlert>.Ok(alert));
|
|
}
|
|
|
|
/// <summary>
|
|
/// Acknowledges an open or escalated alert and emits an outbox event for downstream consumers.
|
|
/// </summary>
|
|
/// <param name="id">Alert id.</param>
|
|
/// <param name="req">Optional acknowledgment note.</param>
|
|
/// <returns>The updated alert.</returns>
|
|
[HttpPost("api/v1/alerts/{id:guid}/acknowledge")]
|
|
[AuthorizePermission(ClinicalPermissions.AlertsAcknowledge)]
|
|
[ProducesResponseType(typeof(ApiResponse<ClinicalAlert>), StatusCodes.Status200OK)]
|
|
[ProducesResponseType(typeof(ApiResponse<object>), StatusCodes.Status404NotFound)]
|
|
[ProducesResponseType(typeof(ApiResponse<object>), StatusCodes.Status409Conflict)]
|
|
public async Task<IActionResult> Acknowledge(Guid id, [FromBody] AcknowledgeAlertRequest req)
|
|
{
|
|
var alert = await _alerts.AcknowledgeAsync(id, req);
|
|
return Ok(ApiResponse<ClinicalAlert>.Ok(alert));
|
|
}
|
|
|
|
/// <summary>
|
|
/// Resolves an acknowledged alert.
|
|
/// </summary>
|
|
/// <param name="id">Alert id.</param>
|
|
/// <returns>The updated alert.</returns>
|
|
[HttpPost("api/v1/alerts/{id:guid}/resolve")]
|
|
[AuthorizePermission(ClinicalPermissions.AlertsResolve)]
|
|
[ProducesResponseType(typeof(ApiResponse<ClinicalAlert>), StatusCodes.Status200OK)]
|
|
[ProducesResponseType(typeof(ApiResponse<object>), StatusCodes.Status404NotFound)]
|
|
[ProducesResponseType(typeof(ApiResponse<object>), StatusCodes.Status409Conflict)]
|
|
public async Task<IActionResult> Resolve(Guid id)
|
|
{
|
|
var alert = await _alerts.ResolveAsync(id);
|
|
return Ok(ApiResponse<ClinicalAlert>.Ok(alert));
|
|
}
|
|
|
|
/// <summary>
|
|
/// Submits clinician feedback for an acknowledged or resolved alert.
|
|
/// One submission per user per alert.
|
|
/// </summary>
|
|
[HttpPost("api/v1/alerts/{id:guid}/feedback")]
|
|
[AuthorizePermission(ClinicalPermissions.AlertsFeedback)]
|
|
[ProducesResponseType(typeof(ApiResponse<object>), StatusCodes.Status201Created)]
|
|
[ProducesResponseType(typeof(ApiResponse<object>), StatusCodes.Status400BadRequest)]
|
|
[ProducesResponseType(typeof(ApiResponse<object>), StatusCodes.Status404NotFound)]
|
|
[ProducesResponseType(typeof(ApiResponse<object>), StatusCodes.Status409Conflict)]
|
|
public async Task<IActionResult> SubmitFeedback(
|
|
Guid id, [FromBody] SubmitAlertFeedbackRequest req)
|
|
{
|
|
var feedback = await _alerts.SubmitFeedbackAsync(id, req.FeedbackType, req.Comment);
|
|
return Created(
|
|
$"/api/v1/alerts/{id}/feedback/{feedback.Id}",
|
|
ApiResponse<object>.Ok(new
|
|
{
|
|
id = feedback.Id,
|
|
alertId = feedback.AlertId,
|
|
feedbackType = feedback.FeedbackType.ToString(),
|
|
comment = feedback.Comment,
|
|
createdAt = feedback.CreatedAt
|
|
}));
|
|
}
|
|
}
|