fix: Missing input validators + No patient update endpoint + Pagination inconsistencies + Missing list/get endpoints

This commit is contained in:
voltsrage
2026-06-21 19:13:13 +08:00
parent 33895122d1
commit a37fad0e57
26 changed files with 932 additions and 22 deletions
@@ -100,7 +100,7 @@ public class AnalyticsController : ControllerBase
[FromQuery] string? q,
[FromQuery] string? department,
[FromQuery] string? status,
[FromQuery] int page = 0,
[FromQuery] int page = 1,
[FromQuery] int pageSize = 20)
{
var result = await _analytics.SearchPatientsAsync(q, department, status, page, pageSize);
@@ -77,7 +77,7 @@ public class ObservationsController : ControllerBase
[FromQuery] string? code,
[FromQuery] DateTimeOffset? from,
[FromQuery] DateTimeOffset? to,
[FromQuery] int limit = 50,
[FromQuery] int limit = 20,
[FromQuery] string? cursor = null)
{
var page = await _query.GetHistoryAsync(encounterId, code, from, to, limit, cursor);
@@ -67,6 +67,22 @@ public class PatientsController : ControllerBase
return Ok(ApiResponse<Patient>.Ok(patient));
}
/// <summary>
/// Updates patient demographics. Only non-null fields are applied.
/// </summary>
/// <param name="id">Patient id.</param>
/// <param name="req">Fields to update.</param>
/// <returns>The updated patient record.</returns>
[HttpPatch("{id:guid}")]
[AuthorizePermission(ClinicalPermissions.PatientsWrite)]
[ProducesResponseType(typeof(ApiResponse<Patient>), StatusCodes.Status200OK)]
[ProducesResponseType(typeof(ApiResponse<object>), StatusCodes.Status404NotFound)]
public async Task<IActionResult> Update(Guid id, [FromBody] UpdatePatientRequest req)
{
var patient = await _patients.UpdateAsync(id, req);
return Ok(ApiResponse<Patient>.Ok(patient));
}
/// <summary>
/// Opens a new active encounter for the patient.
/// </summary>
@@ -25,4 +25,23 @@ public class QsofaController : ControllerBase
var score = await _qsofa.GetCurrentAsync(encounterId);
return Ok(ApiResponse<QsofaCurrentResponse>.Ok(score));
}
/// <summary>
/// Returns cursor-paginated qSOFA alert history for an encounter.
/// </summary>
[HttpGet("history")]
[ProducesResponseType(typeof(ApiResponse<object>), StatusCodes.Status200OK)]
public async Task<IActionResult> History(
Guid encounterId,
[FromQuery] int limit = 20,
[FromQuery] string? cursor = null)
{
var page = await _qsofa.GetHistoryAsync(encounterId, limit, cursor);
return Ok(ApiResponse<object>.Ok(new
{
items = page.Items,
nextCursor = page.NextCursor,
hasMore = page.HasMore
}));
}
}
@@ -0,0 +1,62 @@
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
/// <summary>
/// Data quality reconciliation alerts: unacknowledged critical alerts, pending orders without results,
/// and active inpatients without recent observations.
/// </summary>
[ApiController]
[Route("api/v1/reconciliation-alerts")]
[Produces("application/json")]
[AuthorizePermission(ClinicalPermissions.AlertsRead)]
public class ReconciliationAlertsController : ControllerBase
{
private readonly AppDbContext _db;
public ReconciliationAlertsController(AppDbContext db) => _db = db;
/// <summary>
/// Lists reconciliation alerts with optional filters.
/// </summary>
[HttpGet]
[ProducesResponseType(typeof(ApiResponse<object>), StatusCodes.Status200OK)]
public async Task<IActionResult> List(
[FromQuery] string? checkType,
[FromQuery] bool? resolved,
[FromQuery] int page = 1,
[FromQuery] int pageSize = 20)
{
pageSize = Math.Clamp(pageSize, 1, 100);
var query = _db.ReconciliationAlerts
.AsNoTracking()
.AsQueryable();
if (checkType is not null)
{
var parsed = ReconciliationCheckTypeExtensions.FromDbString(checkType);
query = query.Where(a => a.CheckType == parsed);
}
if (resolved == true)
query = query.Where(a => a.ResolvedAt != null);
else if (resolved == false)
query = query.Where(a => a.ResolvedAt == null);
var total = await query.CountAsync();
var items = await query
.OrderByDescending(a => a.CreatedAt)
.Skip((page - 1) * pageSize)
.Take(pageSize)
.ToListAsync();
return Ok(ApiResponse<object>.Ok(new
{
items,
page,
pageSize,
totalCount = total,
totalPages = (int)Math.Ceiling((double)total / pageSize)
}));
}
}
@@ -13,6 +13,31 @@ public class SepsisBundlesController : ControllerBase
public SepsisBundlesController(ISepsisBundleService bundles) => _bundles = bundles;
/// <summary>
/// Lists sepsis bundles across all encounters with optional status filter.
/// </summary>
[HttpGet("api/v1/sepsis-bundles")]
[ProducesResponseType(typeof(ApiResponse<object>), StatusCodes.Status200OK)]
public async Task<IActionResult> List(
[FromQuery] string? status,
[FromQuery] int page = 1,
[FromQuery] int pageSize = 20)
{
SepsisBundleComplianceStatus? parsed = status is not null
? SepsisBundleComplianceStatusExtensions.FromDbString(status)
: null;
var result = await _bundles.ListAsync(parsed, page, pageSize);
return Ok(ApiResponse<object>.Ok(new
{
items = result.Items,
page = result.Page,
pageSize = result.PageSize,
totalCount = result.TotalCount,
totalPages = result.TotalPages
}));
}
/// <summary>
/// Returns the current (most recent) sepsis bundle for an encounter with elements and linked orders.
/// </summary>