feat(uns): equipment CSV import folded into the tree toolbar
This commit is contained in:
@@ -89,6 +89,17 @@ public sealed record TagEditDto(string TagId, string EquipmentId, string Name, s
|
||||
public sealed record VirtualTagEditDto(string VirtualTagId, string EquipmentId, string Name, string DataType, string ScriptId,
|
||||
bool ChangeTriggered, int? TimerIntervalMs, bool Historize, bool Enabled, byte[] RowVersion);
|
||||
|
||||
/// <summary>
|
||||
/// The outcome of a bulk equipment CSV import: how many rows were inserted, how many were skipped
|
||||
/// (existing MachineCode — the importer is additive-only, never an update), and a per-row error list
|
||||
/// for rows that could not be inserted (unknown line, unknown driver, or a decision-#122 cluster
|
||||
/// mismatch). Skipped rows never appear in <see cref="Errors"/>.
|
||||
/// </summary>
|
||||
/// <param name="Inserted">The count of new Equipment rows added.</param>
|
||||
/// <param name="Skipped">The count of rows skipped because their MachineCode already exists.</param>
|
||||
/// <param name="Errors">The human-readable error strings for rows that failed validation.</param>
|
||||
public sealed record EquipmentImportResult(int Inserted, int Skipped, IReadOnlyList<string> Errors);
|
||||
|
||||
/// <summary>
|
||||
/// Loads the structural portion of the unified-namespace (UNS) browse tree —
|
||||
/// Enterprise → Cluster → Area → Line → Equipment — from the config database.
|
||||
@@ -258,6 +269,21 @@ public interface IUnsTreeService
|
||||
/// <returns>Success, a missing-line failure, a duplicate-MachineCode failure, or a #122 guard failure.</returns>
|
||||
Task<UnsMutationResult> CreateEquipmentAsync(EquipmentInput input, CancellationToken ct = default);
|
||||
|
||||
/// <summary>
|
||||
/// Bulk-imports equipment from a parsed set of <see cref="EquipmentInput"/> rows in a single
|
||||
/// context, applying the same rules as the single-add path: a row whose <c>UnsLineId</c> does not
|
||||
/// exist is an error; a row whose <c>DriverInstanceId</c> is set but does not resolve is an error;
|
||||
/// a driver-bound row whose driver is in a different cluster than its line fails the decision-#122
|
||||
/// guard; and a row whose <c>MachineCode</c> already exists in the DB <em>or</em> earlier in the
|
||||
/// same batch is silently skipped (additive-only — never an update). Inserted rows get a
|
||||
/// system-generated <c>EQ-</c> id and a fresh <c>EquipmentUuid</c>. All inserts are saved once at
|
||||
/// the end.
|
||||
/// </summary>
|
||||
/// <param name="rows">The parsed equipment rows to import.</param>
|
||||
/// <param name="ct">A token to cancel the operation.</param>
|
||||
/// <returns>The insert/skip counts and the per-row error list.</returns>
|
||||
Task<EquipmentImportResult> ImportEquipmentAsync(IReadOnlyList<EquipmentInput> rows, CancellationToken ct = default);
|
||||
|
||||
/// <summary>
|
||||
/// Updates an equipment's mutable fields (driver binding, line, name, MachineCode, external
|
||||
/// ids, and the OPC 40010 identification fields). The decision-#122 driver-cluster guard blocks
|
||||
|
||||
@@ -504,6 +504,81 @@ public sealed class UnsTreeService(IDbContextFactory<OtOpcUaConfigDbContext> dbF
|
||||
return new UnsMutationResult(true, null);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<EquipmentImportResult> ImportEquipmentAsync(
|
||||
IReadOnlyList<EquipmentInput> rows,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
await using var db = await dbFactory.CreateDbContextAsync(ct);
|
||||
|
||||
// Known lines and the MachineCodes already in the fleet, read once up front. The seen-set
|
||||
// tracks MachineCodes inserted earlier in this same batch so two CSV rows that share a
|
||||
// MachineCode skip the duplicate the same way the original ImportEquipment.razor did.
|
||||
var lineSet = (await db.UnsLines.AsNoTracking().Select(l => l.UnsLineId).ToListAsync(ct))
|
||||
.ToHashSet(StringComparer.Ordinal);
|
||||
var existing = (await db.Equipment.AsNoTracking().Select(e => e.MachineCode).ToListAsync(ct))
|
||||
.ToHashSet(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
var inserted = 0;
|
||||
var skipped = 0;
|
||||
var errors = new List<string>();
|
||||
|
||||
foreach (var row in rows)
|
||||
{
|
||||
// Existing MachineCode (in DB or earlier in this batch) → skip, never an error.
|
||||
if (existing.Contains(row.MachineCode))
|
||||
{
|
||||
skipped++;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!lineSet.Contains(row.UnsLineId))
|
||||
{
|
||||
errors.Add($"Row '{row.MachineCode}': UNS line '{row.UnsLineId}' not found.");
|
||||
continue;
|
||||
}
|
||||
|
||||
// Reuse the #122 guard: it reports an unknown DriverInstanceId and a driver/line cluster
|
||||
// mismatch, and is a no-op for driver-less rows.
|
||||
var guard = await CheckDriverClusterGuardAsync(db, row, ct);
|
||||
if (guard is not null)
|
||||
{
|
||||
errors.Add($"Row '{row.MachineCode}': {guard.Value.Error}");
|
||||
continue;
|
||||
}
|
||||
|
||||
var uuid = Guid.NewGuid();
|
||||
var equipmentId = $"EQ-{uuid.ToString("N")[..12]}";
|
||||
|
||||
db.Equipment.Add(new Equipment
|
||||
{
|
||||
EquipmentId = equipmentId,
|
||||
EquipmentUuid = uuid,
|
||||
DriverInstanceId = string.IsNullOrWhiteSpace(row.DriverInstanceId) ? null : row.DriverInstanceId,
|
||||
UnsLineId = row.UnsLineId,
|
||||
Name = row.Name,
|
||||
MachineCode = row.MachineCode,
|
||||
ZTag = string.IsNullOrWhiteSpace(row.ZTag) ? null : row.ZTag,
|
||||
SAPID = string.IsNullOrWhiteSpace(row.SAPID) ? null : row.SAPID,
|
||||
Manufacturer = row.Manufacturer,
|
||||
Model = row.Model,
|
||||
SerialNumber = row.SerialNumber,
|
||||
HardwareRevision = row.HardwareRevision,
|
||||
SoftwareRevision = row.SoftwareRevision,
|
||||
YearOfConstruction = row.YearOfConstruction,
|
||||
AssetLocation = row.AssetLocation,
|
||||
ManufacturerUri = row.ManufacturerUri,
|
||||
DeviceManualUri = row.DeviceManualUri,
|
||||
Enabled = row.Enabled,
|
||||
});
|
||||
existing.Add(row.MachineCode);
|
||||
inserted++;
|
||||
}
|
||||
|
||||
await db.SaveChangesAsync(ct);
|
||||
return new EquipmentImportResult(inserted, skipped, errors);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<UnsMutationResult> UpdateEquipmentAsync(
|
||||
string equipmentId,
|
||||
|
||||
Reference in New Issue
Block a user