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) { var query = _db.WardGateways .AsNoTracking() .Include(g => g.Site) .AsQueryable(); if (!string.IsNullOrEmpty(filter.Status)) { var status = GatewayStatusExtensions.FromDbString(filter.Status); query = query.Where(g => g.Status == status); } if (filter.SiteId is Guid siteId) query = query.Where(g => g.SiteId == siteId); var now = DateTimeOffset.UtcNow; return await query .OrderByDescending(g => g.Status) .ThenByDescending(g => g.LastHeartbeatAt == null) .ThenByDescending(g => g.LastHeartbeatAt) .Select(g => new GatewayFleetItem( g.Id, g.GatewayCode, g.Department, g.Site.SiteCode, g.Site.Name, g.Status.ToDbString(), g.ReportedBufferDepth, g.LastHeartbeatAt, g.LastSyncAt, g.LastHeartbeatAt == null ? null : (double?)(now - g.LastHeartbeatAt.Value).TotalMinutes)) .ToListAsync(ct); } 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); } }