update docs and prep for frontend

This commit is contained in:
voltsrage
2026-06-19 15:44:58 +08:00
parent abc781c9c0
commit 49973271e5
20 changed files with 1071 additions and 26 deletions
@@ -14,8 +14,13 @@ public class EncounterService : IEncounterService
};
private readonly AppDbContext _db;
private readonly IQsofaService _qsofa;
public EncounterService(AppDbContext db) => _db = db;
public EncounterService(AppDbContext db, IQsofaService qsofa)
{
_db = db;
_qsofa = qsofa;
}
public async Task<Encounter> GetByIdAsync(Guid id)
{
@@ -31,6 +36,88 @@ public class EncounterService : IEncounterService
return encounter;
}
public async Task<PagedResult<WardEncounterSummary>> ListAsync(
EncounterStatus? status, Department? department, int page, int pageSize)
{
page = Math.Max(1, page);
pageSize = Math.Clamp(pageSize, 1, 100);
var query = _db.Encounters
.AsNoTracking()
.Include(e => e.Patient)
.AsQueryable();
if (status.HasValue)
query = query.Where(e => e.Status == status.Value);
if (department.HasValue)
query = query.Where(e => e.Department == department.Value);
var total = await query.CountAsync();
var encounters = await query
.OrderByDescending(e => e.AdmittedAt)
.Skip((page - 1) * pageSize)
.Take(pageSize)
.ToListAsync();
if (encounters.Count == 0)
return new PagedResult<WardEncounterSummary>([], page, pageSize, total);
var encounterIds = encounters.Select(e => e.Id).ToList();
var news2ByEncounter = (await _db.News2Scores
.AsNoTracking()
.Where(s => encounterIds.Contains(s.EncounterId))
.OrderByDescending(s => s.CalculatedAt)
.ToListAsync())
.GroupBy(s => s.EncounterId)
.ToDictionary(g => g.Key, g => g.First());
var openAlertCounts = await _db.ClinicalAlerts
.AsNoTracking()
.Where(a => encounterIds.Contains(a.EncounterId) && a.Status == AlertStatus.Open)
.GroupBy(a => a.EncounterId)
.Select(g => new { EncounterId = g.Key, Count = g.Count() })
.ToDictionaryAsync(x => x.EncounterId, x => x.Count);
var bundlesByEncounter = (await _db.SepsisBundles
.AsNoTracking()
.Where(b => encounterIds.Contains(b.EncounterId))
.OrderByDescending(b => b.RecognizedAt)
.ToListAsync())
.GroupBy(b => b.EncounterId)
.ToDictionary(g => g.Key, g => g.First());
var summaries = new List<WardEncounterSummary>(encounters.Count);
foreach (var encounter in encounters)
{
news2ByEncounter.TryGetValue(encounter.Id, out var news2);
bundlesByEncounter.TryGetValue(encounter.Id, out var bundle);
openAlertCounts.TryGetValue(encounter.Id, out var openCount);
var qsofaScore = await _qsofa.GetActiveCriteriaCountAsync(encounter.Id);
summaries.Add(new WardEncounterSummary(
encounter.Id,
encounter.PatientId,
encounter.Patient.Mrn,
encounter.Patient.FirstName,
encounter.Patient.LastName,
encounter.RoomBed,
encounter.Department,
encounter.Status,
news2?.TotalScore,
news2?.RiskLevel,
qsofaScore,
bundle is not null && bundle.ComplianceStatus != SepsisBundleComplianceStatus.Compliant,
bundle?.ComplianceStatus,
openCount));
}
return new PagedResult<WardEncounterSummary>(summaries, page, pageSize, total);
}
public async Task<EncounterStatusTransitionResult> TransitionStatusAsync(
Guid encounterId, EncounterStatus targetStatus, string? dischargeDiagnosis = null)
{
@@ -1,6 +1,8 @@
public interface IEncounterService
{
Task<Encounter> GetByIdAsync(Guid id);
Task<PagedResult<WardEncounterSummary>> ListAsync(
EncounterStatus? status, Department? department, int page, int pageSize);
Task<EncounterStatusTransitionResult> TransitionStatusAsync(
Guid encounterId, EncounterStatus targetStatus, string? dischargeDiagnosis = null);
Task<object> GetTimelineAsync(Guid encounterId);
@@ -0,0 +1,5 @@
public interface IQsofaService
{
Task<QsofaCurrentResponse> GetCurrentAsync(Guid encounterId);
Task<int> GetActiveCriteriaCountAsync(Guid encounterId);
}
@@ -0,0 +1,54 @@
using Microsoft.EntityFrameworkCore;
using StackExchange.Redis;
public class QsofaService : IQsofaService
{
private readonly AppDbContext _db;
private readonly IConnectionMultiplexer _redis;
public QsofaService(AppDbContext db, IConnectionMultiplexer redis)
{
_db = db;
_redis = redis;
}
public async Task<QsofaCurrentResponse> GetCurrentAsync(Guid encounterId)
{
await EnsureEncounterExistsAsync(encounterId);
var values = await ReadCriterionValuesAsync(encounterId);
return BuildResponse(values);
}
public async Task<int> GetActiveCriteriaCountAsync(Guid encounterId)
{
var values = await ReadCriterionValuesAsync(encounterId);
return QsofaCalculator.CountActiveCriteria(values);
}
private async Task EnsureEncounterExistsAsync(Guid encounterId)
{
var exists = await _db.Encounters.AnyAsync(e => e.Id == encounterId);
if (!exists)
throw new NotFoundException("Encounter not found.", "ENCOUNTER_NOT_FOUND");
}
private async Task<RedisValue[]> ReadCriterionValuesAsync(Guid encounterId)
{
var cache = _redis.GetDatabase();
return await cache.StringGetAsync(QsofaCalculator.AllCriterionKeys(encounterId));
}
private static QsofaCurrentResponse BuildResponse(RedisValue[] values)
{
return new QsofaCurrentResponse(
QsofaCalculator.CountActiveCriteria(values),
new QsofaCriteriaState(
ParseOptionalDecimal(values[0]),
ParseOptionalDecimal(values[1]),
ParseOptionalDecimal(values[2])));
}
private static decimal? ParseOptionalDecimal(RedisValue value) =>
value.HasValue ? decimal.Parse(value.ToString()) : null;
}