133 lines
5.1 KiB
C#
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);
|
|
}
|