Files
voltsrage 80b009fd23
CI / backend (push) Successful in 8m52s
CI / frontend (push) Failing after 1m39s
feature: Self-Service Clinical Testing Sessions
2026-08-06 04:03:04 +08:00

133 lines
5.1 KiB
C#

using Microsoft.EntityFrameworkCore;
public class AlertQualityMetricsService : IAlertQualityMetricsService
{
private readonly AppDbContext _db;
public AlertQualityMetricsService(AppDbContext db) => _db = db;
public async Task<IReadOnlyList<AlertQualityMetricResponse>> ListAsync(
AlertType? alertType, DateTimeOffset from, DateTimeOffset to)
{
var query = _db.AlertQualityMetrics
.AsNoTracking()
.Where(m => m.WindowStart >= from && m.WindowEnd <= to);
if (alertType.HasValue)
query = query.Where(m => m.AlertType == alertType.Value);
var rows = await query
.OrderByDescending(m => m.WindowStart)
.ThenBy(m => m.AlertType)
.ToListAsync();
return rows.Select(Map).ToList();
}
public async Task<AlertQualitySummaryResponse> GetSummaryAsync(
DateTimeOffset from, DateTimeOffset to)
{
var snapshots = await _db.AlertQualityMetrics
.AsNoTracking()
.Where(m => m.WindowStart >= from && m.WindowEnd <= to)
.ToListAsync();
if (snapshots.Count == 0)
{
return new AlertQualitySummaryResponse(
from, to, 0, 0, 0, 0, 0, 0, 0, 0);
}
var totalAlerts = snapshots.Sum(s => s.TotalAlerts);
var totalFeedback = snapshots.Sum(s => s.FeedbackCount);
var totalAcknowledged = snapshots.Sum(s => s.AcknowledgedCount);
var totalUseful = snapshots.Sum(s => s.FeedbackUsefulCount);
var totalFalsePositive = snapshots.Sum(s => s.FeedbackFalsePositiveCount);
var totalWouldAct = snapshots.Sum(s => s.FeedbackWouldActCount);
var weightedAckSeconds = snapshots.Sum(s => s.AvgSecondsToAcknowledge * s.TotalAlerts);
var weightedResolveSeconds = snapshots.Sum(s => s.AvgSecondsToResolution * s.ResolvedCount);
return new AlertQualitySummaryResponse(
PeriodStart: from,
PeriodEnd: to,
TotalAlerts: totalAlerts,
TotalFeedback: totalFeedback,
AcknowledgementRate: totalAlerts == 0 ? 0 : (double)totalAcknowledged / totalAlerts,
FalsePositiveRate: totalFeedback == 0 ? 0 : (double)totalFalsePositive / totalFeedback,
UsefulRate: totalFeedback == 0 ? 0 : (double)totalUseful / totalFeedback,
WouldActRate: totalFeedback == 0 ? 0 : (double)totalWouldAct / totalFeedback,
AvgSecondsToAcknowledge: totalAlerts == 0 ? 0 : weightedAckSeconds / totalAlerts,
AvgSecondsToResolution: snapshots.Sum(s => s.ResolvedCount) == 0
? 0
: weightedResolveSeconds / snapshots.Sum(s => s.ResolvedCount));
}
public async Task<IReadOnlyList<AlertFeedbackQualityRow>> ListFeedbackAsync(
DateTimeOffset periodStart, DateTimeOffset periodEnd, string? scenarioId)
{
// Left-join ClinicalAlert → SimulationRun on EncounterId for attribution.
var query =
from f in _db.AlertFeedbacks.AsNoTracking()
join a in _db.ClinicalAlerts.AsNoTracking() on f.AlertId equals a.Id
join r in _db.SimulationRuns.AsNoTracking()
on a.EncounterId equals r.EncounterId into runs
from r in runs.DefaultIfEmpty()
where f.CreatedAt >= periodStart && f.CreatedAt <= periodEnd
select new { Feedback = f, Alert = a, Run = r };
if (!string.IsNullOrWhiteSpace(scenarioId))
{
var needle = scenarioId.ToLowerInvariant();
query = query.Where(x =>
x.Run != null
&& x.Run.ScenarioId != null
&& x.Run.ScenarioId.ToLower() == needle);
}
var rows = await query
.OrderByDescending(x => x.Feedback.CreatedAt)
.ToListAsync();
// Distinct by feedback id in case multiple SimulationRun rows share an EncounterId.
return rows
.GroupBy(x => x.Feedback.Id)
.Select(g =>
{
var x = g.First();
return new AlertFeedbackQualityRow(
x.Feedback.Id,
x.Alert.Id,
x.Alert.AlertType.ToDbString(),
x.Feedback.FeedbackType.ToDbString(),
x.Feedback.Comment,
x.Feedback.CreatedAt,
x.Run?.ScenarioId,
x.Run?.SessionId);
})
.ToList();
}
private static AlertQualityMetricResponse Map(AlertQualityMetric m) =>
new(
m.Id,
m.AlertType.ToString(),
m.WindowStart,
m.WindowEnd,
m.TotalAlerts,
m.AcknowledgedCount,
m.ResolvedCount,
m.EscalatedCount,
m.FeedbackUsefulCount,
m.FeedbackFalsePositiveCount,
m.FeedbackWouldActCount,
m.FeedbackCount,
m.AcknowledgementRate,
m.FalsePositiveRate,
m.UsefulRate,
m.WouldActRate,
m.AvgSecondsToAcknowledge,
m.AvgSecondsToResolution,
m.ComputedAt);
}