Files

110 lines
5.0 KiB
C#

using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
/// <summary>
/// Elasticsearch-backed analytics queries for population, trends, alerts, and patient search.
/// </summary>
[ApiController]
[Route("api/v1/analytics")]
[Produces("application/json")]
[AuthorizePermission(ClinicalPermissions.AnalyticsRead)]
public class AnalyticsController : ControllerBase
{
private readonly IAnalyticsService _analytics;
public AnalyticsController(IAnalyticsService analytics) => _analytics = analytics;
/// <summary>
/// Counts distinct patients whose observations for a code exceed or fall below a threshold in a time window.
/// </summary>
/// <param name="code">Observation code (e.g. HEART_RATE).</param>
/// <param name="threshold">Numeric threshold value.</param>
/// <param name="direction">above or below.</param>
/// <param name="from">Optional start of recorded-at range.</param>
/// <param name="to">Optional end of recorded-at range.</param>
/// <returns>Cardinality of unique patients matching the criteria.</returns>
[HttpGet("population")]
[ProducesResponseType(typeof(ApiResponse<object>), StatusCodes.Status200OK)]
[ProducesResponseType(typeof(ApiResponse<object>), StatusCodes.Status400BadRequest)]
[ProducesResponseType(typeof(ApiResponse<object>), StatusCodes.Status500InternalServerError)]
public async Task<IActionResult> Population(
[FromQuery] string? code,
[FromQuery] decimal threshold,
[FromQuery] string direction, // "above" | "below"
[FromQuery] DateTimeOffset? from,
[FromQuery] DateTimeOffset? to)
{
if (string.IsNullOrWhiteSpace(code))
return BadRequest(ApiResponse<object>.Fail(400, "code is required.", "MISSING_CODE"));
var result = await _analytics.GetPopulationAsync(code, threshold, direction, from, to);
return Ok(ApiResponse<object>.Ok(result));
}
/// <summary>
/// Returns hourly observation volume for an encounter and observation code.
/// </summary>
/// <param name="encounterId">Encounter id.</param>
/// <param name="code">Observation code.</param>
/// <param name="from">Optional start of recorded-at range.</param>
/// <param name="to">Optional end of recorded-at range.</param>
/// <returns>Hourly trend buckets with document counts.</returns>
[HttpGet("observations/trend")]
[ProducesResponseType(typeof(ApiResponse<object>), StatusCodes.Status200OK)]
[ProducesResponseType(typeof(ApiResponse<object>), StatusCodes.Status500InternalServerError)]
public async Task<IActionResult> ObservationTrend(
[FromQuery] Guid encounterId,
[FromQuery] string code,
[FromQuery] DateTimeOffset? from,
[FromQuery] DateTimeOffset? to)
{
var result = await _analytics.GetObservationTrendAsync(encounterId, code, from, to);
return Ok(ApiResponse<object>.Ok(result));
}
/// <summary>
/// Summarizes alert volume by department over a time window.
/// </summary>
/// <param name="severity">Optional severity filter.</param>
/// <param name="department">Optional department filter.</param>
/// <param name="from">Optional start of triggered-at range.</param>
/// <param name="to">Optional end of triggered-at range.</param>
/// <returns>Department-level counts (severity breakdown omitted in current implementation).</returns>
[HttpGet("alerts/summary")]
[ProducesResponseType(typeof(ApiResponse<object>), StatusCodes.Status200OK)]
[ProducesResponseType(typeof(ApiResponse<object>), StatusCodes.Status500InternalServerError)]
public async Task<IActionResult> AlertSummary(
[FromQuery] string? severity,
[FromQuery] string? department,
[FromQuery] DateTimeOffset? from,
[FromQuery] DateTimeOffset? to)
{
var result = await _analytics.GetAlertSummaryAsync(severity, department, from, to);
return Ok(ApiResponse<object>.Ok(result));
}
/// <summary>
/// Searches patient/encounter documents by MRN, name, department, or status.
/// </summary>
/// <param name="q">Free-text query (MRN, name, or department).</param>
/// <param name="department">Optional department filter.</param>
/// <param name="status">Optional encounter status filter.</param>
/// <param name="page">Page number (0-based).</param>
/// <param name="pageSize">Results per page.</param>
/// <returns>Matching patient/encounter documents.</returns>
[HttpGet("patients")]
[ProducesResponseType(typeof(ApiResponse<object>), StatusCodes.Status200OK)]
[ProducesResponseType(typeof(ApiResponse<object>), StatusCodes.Status500InternalServerError)]
public async Task<IActionResult> PatientSearch(
[FromQuery] string? q,
[FromQuery] string? department,
[FromQuery] string? status,
[FromQuery] int page = 1,
[FromQuery] int pageSize = 20)
{
var result = await _analytics.SearchPatientsAsync(q, department, status, page, pageSize);
return Ok(ApiResponse<object>.Ok(result));
}
}