144 lines
5.0 KiB
C#
144 lines
5.0 KiB
C#
using Microsoft.AspNetCore.Authorization;
|
|
using Microsoft.AspNetCore.Mvc;
|
|
using Microsoft.Extensions.Options;
|
|
|
|
[ApiController]
|
|
[Produces("application/json")]
|
|
[Authorize]
|
|
public class AlertQualityMetricsController : ControllerBase
|
|
{
|
|
private readonly IAlertQualityMetricsService _metrics;
|
|
private readonly SimulationOptions _simulation;
|
|
|
|
public AlertQualityMetricsController(
|
|
IAlertQualityMetricsService metrics,
|
|
IOptions<SimulationOptions> simulation)
|
|
{
|
|
_metrics = metrics;
|
|
_simulation = simulation.Value;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns alert quality metric snapshots for a time range, optionally filtered by alert type.
|
|
/// </summary>
|
|
[HttpGet("api/v1/alerts/quality-metrics")]
|
|
[AuthorizePermission(ClinicalPermissions.AnalyticsRead)]
|
|
[ProducesResponseType(typeof(ApiResponse<object>), StatusCodes.Status200OK)]
|
|
[ProducesResponseType(typeof(ApiResponse<object>), StatusCodes.Status400BadRequest)]
|
|
public async Task<IActionResult> List(
|
|
[FromQuery] string? alertType,
|
|
[FromQuery] DateTimeOffset? from,
|
|
[FromQuery] DateTimeOffset? to)
|
|
{
|
|
AlertType? parsedType = null;
|
|
if (!string.IsNullOrEmpty(alertType))
|
|
{
|
|
try
|
|
{
|
|
parsedType = AlertTypeExtensions.FromDbString(alertType);
|
|
}
|
|
catch (ArgumentOutOfRangeException)
|
|
{
|
|
return BadRequest(ApiResponse<object>.Fail(
|
|
400, "Invalid alert type filter.", "INVALID_ALERT_TYPE"));
|
|
}
|
|
}
|
|
|
|
var periodEnd = to ?? DateTimeOffset.UtcNow;
|
|
var periodStart = from ?? periodEnd.AddDays(-7);
|
|
if (periodStart >= periodEnd)
|
|
{
|
|
return BadRequest(ApiResponse<object>.Fail(
|
|
400, "'from' must be before 'to'.", "INVALID_DATE_RANGE"));
|
|
}
|
|
|
|
var items = await _metrics.ListAsync(parsedType, periodStart, periodEnd);
|
|
return Ok(ApiResponse<object>.Ok(new
|
|
{
|
|
periodStart,
|
|
periodEnd,
|
|
items
|
|
}));
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns aggregate alert quality rates across all alert types for a time range.
|
|
/// </summary>
|
|
[HttpGet("api/v1/alerts/quality-metrics/summary")]
|
|
[AuthorizePermission(ClinicalPermissions.AnalyticsRead)]
|
|
[ProducesResponseType(typeof(ApiResponse<AlertQualitySummaryResponse>), StatusCodes.Status200OK)]
|
|
public async Task<IActionResult> Summary(
|
|
[FromQuery] DateTimeOffset? from,
|
|
[FromQuery] DateTimeOffset? to)
|
|
{
|
|
var periodEnd = to ?? DateTimeOffset.UtcNow;
|
|
var periodStart = from ?? periodEnd.AddDays(-7);
|
|
|
|
var summary = await _metrics.GetSummaryAsync(periodStart, periodEnd);
|
|
return Ok(ApiResponse<AlertQualitySummaryResponse>.Ok(summary));
|
|
}
|
|
|
|
/// <summary>
|
|
/// Per-alert feedback rows with optional scenario attribution (when simulation is enabled).
|
|
/// Filter with <c>?scenarioId=</c> to limit to one simulated scenario.
|
|
/// </summary>
|
|
[HttpGet("api/v1/alerts/quality-metrics/feedback")]
|
|
[AuthorizePermission(ClinicalPermissions.AnalyticsRead)]
|
|
[ProducesResponseType(typeof(ApiResponse<object>), StatusCodes.Status200OK)]
|
|
[ProducesResponseType(typeof(ApiResponse<object>), StatusCodes.Status400BadRequest)]
|
|
public async Task<IActionResult> ListFeedback(
|
|
[FromQuery] string? scenarioId,
|
|
[FromQuery] DateTimeOffset? from,
|
|
[FromQuery] DateTimeOffset? to)
|
|
{
|
|
var periodEnd = to ?? DateTimeOffset.UtcNow;
|
|
var periodStart = from ?? periodEnd.AddDays(-7);
|
|
if (periodStart >= periodEnd)
|
|
{
|
|
return BadRequest(ApiResponse<object>.Fail(
|
|
400, "'from' must be before 'to'.", "INVALID_DATE_RANGE"));
|
|
}
|
|
|
|
// Scenario filter only applies when simulation attribution is available.
|
|
var filterScenario = _simulation.Enabled ? scenarioId : null;
|
|
var rows = await _metrics.ListFeedbackAsync(periodStart, periodEnd, filterScenario);
|
|
|
|
if (_simulation.Enabled)
|
|
{
|
|
var withAttribution = rows.Select(r => new
|
|
{
|
|
id = r.Id,
|
|
alertId = r.AlertId,
|
|
alertType = r.AlertType,
|
|
feedbackType = r.FeedbackType,
|
|
comment = r.Comment,
|
|
createdAt = r.CreatedAt,
|
|
scenarioId = r.ScenarioId,
|
|
sessionId = r.SessionId,
|
|
});
|
|
return Ok(ApiResponse<object>.Ok(new
|
|
{
|
|
periodStart,
|
|
periodEnd,
|
|
items = withAttribution,
|
|
}));
|
|
}
|
|
|
|
var withoutAttribution = rows.Select(r => new
|
|
{
|
|
id = r.Id,
|
|
alertId = r.AlertId,
|
|
alertType = r.AlertType,
|
|
feedbackType = r.FeedbackType,
|
|
comment = r.Comment,
|
|
createdAt = r.CreatedAt,
|
|
});
|
|
return Ok(ApiResponse<object>.Ok(new
|
|
{
|
|
periodStart,
|
|
periodEnd,
|
|
items = withoutAttribution,
|
|
}));
|
|
}
|
|
}
|