using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; /// /// Elasticsearch-backed analytics queries for population, trends, alerts, and patient search. /// [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; /// /// 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 = 1, [FromQuery] int pageSize = 20) { var result = await _analytics.SearchPatientsAsync(q, department, status, page, pageSize); return Ok(ApiResponse.Ok(result)); } }