using Dapper; using Microsoft.EntityFrameworkCore; using Npgsql; public class OperationsService : IOperationsService { private readonly AppDbContext _db; private readonly IClinicalSyncService _sync; public OperationsService(AppDbContext db, IClinicalSyncService sync) { _db = db; _sync = sync; } public async Task> GetGatewayFleetAsync( GatewayFleetFilter filter, CancellationToken ct) { await using var conn = new NpgsqlConnection(_db.Database.GetConnectionString()); await conn.OpenAsync(ct); const string sql = """ SELECT g.id, g.gateway_code AS GatewayCode, g.department AS Department, s.site_code AS SiteCode, s.name AS SiteName, g.status AS Status, g.reported_buffer_depth AS ReportedBufferDepth, g.last_heartbeat_at AS LastHeartbeatAt, g.last_sync_at AS LastSyncAt, EXTRACT(EPOCH FROM (NOW() - g.last_heartbeat_at)) / 60 AS MinutesSinceHeartbeat FROM ward_gateways g JOIN clinical_sites s ON s.id = g.site_id WHERE (@Status IS NULL OR g.status = @Status) AND (@SiteId IS NULL OR g.site_id = @SiteId) ORDER BY g.status DESC, MinutesSinceHeartbeat DESC NULLS LAST """; var rows = await conn.QueryAsync(sql, new { Status = filter.Status, SiteId = filter.SiteId }); return rows.ToList(); } public async Task GetGatewayDetailAsync(Guid gatewayId, CancellationToken ct) { var fleet = await GetGatewayFleetAsync(new GatewayFleetFilter(null, null), ct); var gateway = fleet.FirstOrDefault(g => g.Id == gatewayId) ?? throw new NotFoundException("Gateway not found.", "GATEWAY_NOT_FOUND"); var siteId = await _db.WardGateways .Where(g => g.Id == gatewayId) .Select(g => g.SiteId) .FirstAsync(ct); var history = await _sync.GetSyncHistoryAsync(siteId, gatewayId, 10, ct); var lastConflictBatch = await _db.ClinicalSyncBatches .AsNoTracking() .Where(b => b.GatewayId == gatewayId && b.Status == ClinicalSyncBatchStatus.Conflict) .OrderByDescending(b => b.SubmittedAt) .Include(b => b.Conflicts) .FirstOrDefaultAsync(ct); var pendingConflicts = lastConflictBatch?.Conflicts.Count ?? 0; return new GatewayDetailResponse(gateway, history, pendingConflicts); } public async Task GetSiteSummaryAsync(Guid siteId, CancellationToken ct) { var siteExists = await _db.ClinicalSites.AnyAsync(s => s.Id == siteId, ct); if (!siteExists) throw new NotFoundException("Site not found.", "SITE_NOT_FOUND"); await using var conn = new NpgsqlConnection(_db.Database.GetConnectionString()); await conn.OpenAsync(ct); const string sql = """ SELECT COUNT(*) AS TotalGateways, COUNT(*) FILTER (WHERE status = 'ONLINE') AS Online, COUNT(*) FILTER (WHERE status = 'DEGRADED') AS Degraded, COUNT(*) FILTER (WHERE status = 'OFFLINE') AS Offline, COALESCE(SUM(reported_buffer_depth), 0) AS TotalBufferedEvents FROM ward_gateways WHERE site_id = @SiteId """; var row = await conn.QuerySingleAsync(sql, new { SiteId = siteId }); return new SiteGatewaySummaryResponse( siteId, (int)row.totalgateways, (int)row.online, (int)row.degraded, (int)row.offline, (int)row.totalbufferedevents); } }