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