using System.Security.Cryptography; using System.Text; using Microsoft.EntityFrameworkCore; using ZB.MOM.WW.OtOpcUa.Commons.Types; using ZB.MOM.WW.OtOpcUa.Configuration; using ZB.MOM.WW.OtOpcUa.Configuration.Entities; using ZB.MOM.WW.OtOpcUa.Configuration.Enums; namespace ZB.MOM.WW.OtOpcUa.AdminUI.Uns; /// /// Default . Reads the structural rows with a handful of /// untracked queries, computes per-equipment tag/virtual-tag counts, and hands the flat /// rows to the pure to nest into the browse tree. /// /// /// Each call creates and disposes its own context via the pooled factory — the same pattern /// every AdminUI page uses — so the service is safe to register as Scoped and used per /// Blazor circuit. /// public sealed class UnsTreeService(IDbContextFactory dbFactory) : IUnsTreeService { /// public async Task> LoadStructureAsync(CancellationToken ct = default) { await using var db = await dbFactory.CreateDbContextAsync(ct); var clusters = await db.ServerClusters .AsNoTracking() .Select(c => new ClusterRow(c.ClusterId, c.Enterprise, c.Site, c.Name)) .ToListAsync(ct); var areas = await db.UnsAreas .AsNoTracking() .Select(a => new AreaRow(a.UnsAreaId, a.ClusterId, a.Name)) .ToListAsync(ct); var lines = await db.UnsLines .AsNoTracking() .Select(l => new LineRow(l.UnsLineId, l.UnsAreaId, l.Name)) .ToListAsync(ct); var equipmentRows = await db.Equipment .AsNoTracking() .Select(e => new { e.EquipmentId, e.UnsLineId, e.MachineCode, e.Name, }) .ToListAsync(ct); // Per-equipment driver-tag counts: the equipment↔Tag binding was retired in v3 // (Tag is now Raw-only; authoring moved to the Raw tree in Batch 2/3). No binding // exists to count against, so every equipment reports zero driver tags. var tagCounts = new Dictionary(StringComparer.Ordinal); // Per-equipment virtual-tag counts (virtual tags with no equipment are excluded). var vtagCounts = (await db.VirtualTags .AsNoTracking() .Where(v => v.EquipmentId != null) .GroupBy(v => v.EquipmentId) .Select(g => new { EquipmentId = g.Key!, Count = g.Count() }) .ToListAsync(ct)) .ToDictionary(x => x.EquipmentId, x => x.Count, StringComparer.Ordinal); var equipment = equipmentRows .Select(e => new EquipmentRow( e.EquipmentId, e.UnsLineId, e.MachineCode, e.Name, tagCounts.GetValueOrDefault(e.EquipmentId), vtagCounts.GetValueOrDefault(e.EquipmentId))) .ToList(); return UnsTreeAssembly.Build(clusters, areas, lines, equipment); } /// public Task> LoadTagsForEquipmentAsync(string equipmentId, CancellationToken ct = default) { // The equipment↔Tag binding was retired in v3 (Tag is now Raw-only; authoring moved to // the Raw tree in Batch 2/3). No equipment-bound tags exist to list. return Task.FromResult>(Array.Empty()); } /// public async Task> LoadVirtualTagsForEquipmentAsync(string equipmentId, CancellationToken ct = default) { await using var db = await dbFactory.CreateDbContextAsync(ct); return await db.VirtualTags.AsNoTracking() .Where(v => v.EquipmentId == equipmentId) .OrderBy(v => v.Name) .Select(v => new EquipmentVirtualTagRow(v.VirtualTagId, v.Name, v.DataType, v.ScriptId, v.Enabled)) .ToListAsync(ct); } /// public async Task LoadAreaAsync(string unsAreaId, CancellationToken ct = default) { await using var db = await dbFactory.CreateDbContextAsync(ct); return await db.UnsAreas .AsNoTracking() .Where(a => a.UnsAreaId == unsAreaId) .Select(a => new AreaEditDto(a.UnsAreaId, a.Name, a.Notes, a.ClusterId, a.RowVersion)) .FirstOrDefaultAsync(ct); } /// public async Task LoadLineAsync(string unsLineId, CancellationToken ct = default) { await using var db = await dbFactory.CreateDbContextAsync(ct); return await db.UnsLines .AsNoTracking() .Where(l => l.UnsLineId == unsLineId) .Select(l => new LineEditDto(l.UnsLineId, l.UnsAreaId, l.Name, l.Notes, l.RowVersion)) .FirstOrDefaultAsync(ct); } /// public async Task LoadEquipmentAsync(string equipmentId, CancellationToken ct = default) { await using var db = await dbFactory.CreateDbContextAsync(ct); return await db.Equipment .AsNoTracking() .Where(e => e.EquipmentId == equipmentId) .Select(e => new EquipmentEditDto( e.EquipmentId, e.Name, e.MachineCode, e.UnsLineId, // Equipment↔driver binding retired in v3 — no DriverInstanceId column remains. null, e.ZTag, e.SAPID, e.Manufacturer, e.Model, e.SerialNumber, e.HardwareRevision, e.SoftwareRevision, e.YearOfConstruction, e.AssetLocation, e.ManufacturerUri, e.DeviceManualUri, e.Enabled, e.RowVersion)) .FirstOrDefaultAsync(ct); } /// public async Task LoadTagAsync(string tagId, CancellationToken ct = default) { await using var db = await dbFactory.CreateDbContextAsync(ct); return await db.Tags .AsNoTracking() .Where(t => t.TagId == tagId) .Select(t => new TagEditDto( t.TagId, // Tag is Raw-only in v3 — equipment binding + driver binding retired. string.Empty, t.Name, string.Empty, t.DataType, t.AccessLevel, t.WriteIdempotent, t.PollGroupId, t.TagConfig, t.RowVersion)) .FirstOrDefaultAsync(ct); } /// public async Task LoadVirtualTagAsync(string virtualTagId, CancellationToken ct = default) { await using var db = await dbFactory.CreateDbContextAsync(ct); return await db.VirtualTags .AsNoTracking() .Where(v => v.VirtualTagId == virtualTagId) .Select(v => new VirtualTagEditDto( v.VirtualTagId, v.EquipmentId, v.Name, v.DataType, v.ScriptId, v.ChangeTriggered, v.TimerIntervalMs, v.Historize, v.Enabled, v.RowVersion)) .FirstOrDefaultAsync(ct); } /// public async Task> LoadDriversForClusterAsync( string clusterId, CancellationToken ct = default) { await using var db = await dbFactory.CreateDbContextAsync(ct); var drivers = await db.DriverInstances .AsNoTracking() .Where(d => d.ClusterId == clusterId) .OrderBy(d => d.DriverInstanceId) .Select(d => new { d.DriverInstanceId, d.Name, d.DriverType }) .ToListAsync(ct); return drivers .Select(d => (d.DriverInstanceId, Display: $"{d.DriverInstanceId} — {d.Name} ({d.DriverType})")) .ToList(); } /// public async Task LoadEquipmentPickContextAsync(string? lineId, CancellationToken ct = default) { var empty = new EquipmentPickContext(Array.Empty<(string, string)>(), Array.Empty<(string, string)>()); if (string.IsNullOrEmpty(lineId)) return empty; await using var db = await dbFactory.CreateDbContextAsync(ct); // line -> area -> clusterId var clusterId = await (from l in db.UnsLines.AsNoTracking() join a in db.UnsAreas.AsNoTracking() on l.UnsAreaId equals a.UnsAreaId where l.UnsLineId == lineId select a.ClusterId).FirstOrDefaultAsync(ct); if (string.IsNullOrEmpty(clusterId)) return empty; // all lines in that cluster (for the line