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 })); } }