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