@page "/import" @inject MeterVault.Infrastructure.Import.ReferenceDataImporter ReferenceImporter @inject MeterVault.Infrastructure.Import.CsvImporter CsvImporter @inject MeterVault.Infrastructure.Import.ImportService ImportService @inject Microsoft.EntityFrameworkCore.IDbContextFactory DbFactory @inject IDialogService Dialogs @inject ISnackbar Snackbar @using MeterVault.Infrastructure.Import @using MeterVault.Infrastructure.Persistence @using Microsoft.EntityFrameworkCore MeterVault — @S.Nav_Import @S.Nav_Import @S.Import_ReferenceDataset @S.Import_ReferenceDatasetHelp @(_referenceLoaded ? S.Import_ReferenceLoaded : S.Import_LoadReferenceData) @if (_loadingReference) { }
@S.Import_YourOwnCsv @S.Import_MappingWizard
@S.Import_YourOwnCsvHelp @S.Import_ProfileElectricity @S.Import_ProfileWater @S.Import_ProfileHeatingOil @S.Import_ProfileCosts @S.Import_DryRunReferenceSheet
@if (_preview is not null) { @S.Common_Preview
@S.Common_ReadingsLabel @_preview.Readings.Count @S.Common_EventsLabel @_preview.Events.Count @S.Common_ManualCostsLabel @_preview.ManualCosts.Count @S.Common_SkippedRowsLabel @_preview.SkippedRows
@if (_preview.Warnings.Count > 0) { @foreach (var warning in _preview.Warnings.Take(50)) { @warning.Display() } }
} @S.Import_RecentImports @if (_batches.Count == 0) { @S.Import_NoImportsYet } else { #@S.Import_ColumnSource@S.Import_ColumnWritesTo@S.Import_ColumnRows@S.Import_ColumnImported@S.Common_Status @foreach (var batch in _batches) { @batch.Id @(batch.SourceName ?? "—") @* What the batch wrote to, so an import can be checked where it landed — and a revert weighed against what it will take away. *@ @if (_targets.TryGetValue(batch.Id, out var targets)) { @foreach (var meter in targets.Meters) { @meter.Name } @foreach (var category in targets.Categories) { @category } } else { } @batch.RowCount @batch.CreatedAt.LocalDateTime.ToString("yyyy-MM-dd HH:mm") @if (batch.RevertedAt is not null) { @S.Import_StatusReverted } else { @S.Import_StatusActive } @S.Import_Revert } }
@code { private bool _loadingReference; private bool _referenceLoaded; private string _profileName = "Strom"; private StagedImport? _preview; private List _batches = []; private Dictionary _targets = []; private int? _reverting; protected override async Task OnInitializedAsync() { _referenceLoaded = await ReferenceImporter.IsLoadedAsync(); await LoadBatchesAsync(); } private async Task LoadBatchesAsync() { await using var db = await DbFactory.CreateDbContextAsync(); _batches = await db.ImportBatches.AsNoTracking() .OrderByDescending(b => b.Id) .Take(25) .ToListAsync(); _targets = await LoadTargetsAsync(db, [.. _batches.Where(b => b.RevertedAt is null).Select(b => b.Id)]); } /// /// The meters and cost categories each batch wrote rows for. Read from the rows themselves rather /// than the stored mapping, which the reference import does not have and which cannot say what a /// partly failed or reverted batch actually left behind. /// private static async Task> LoadTargetsAsync(MeterVaultDbContext db, List batchIds) { if (batchIds.Count == 0) { return []; } var readingMeters = await db.Readings.AsNoTracking() .Where(r => r.ImportBatchId != null && batchIds.Contains(r.ImportBatchId.Value)) .Select(r => new { Batch = r.ImportBatchId!.Value, r.MeterId }) .Distinct() .ToListAsync(); var eventMeters = await db.MeterEvents.AsNoTracking() .Where(e => e.ImportBatchId != null && batchIds.Contains(e.ImportBatchId.Value)) .Select(e => new { Batch = e.ImportBatchId!.Value, e.MeterId }) .Distinct() .ToListAsync(); var costs = await db.ManualCosts.AsNoTracking() .Where(c => c.ImportBatchId != null && batchIds.Contains(c.ImportBatchId.Value)) .Select(c => new { Batch = c.ImportBatchId!.Value, c.MeterId, c.CategoryId }) .Distinct() .ToListAsync(); var meterPairs = readingMeters.Select(r => (r.Batch, MeterId: (int?)r.MeterId)) .Concat(eventMeters.Select(e => (e.Batch, MeterId: (int?)e.MeterId))) .Concat(costs.Select(c => (c.Batch, c.MeterId))) .Where(p => p.MeterId is not null) .Select(p => (p.Batch, MeterId: p.MeterId!.Value)) .Distinct() .ToList(); var categoryPairs = costs.Where(c => c.CategoryId is not null).Select(c => (c.Batch, CategoryId: c.CategoryId!.Value)).Distinct().ToList(); var meterIds = meterPairs.Select(p => p.MeterId).Distinct().ToList(); var categoryIds = categoryPairs.Select(p => p.CategoryId).Distinct().ToList(); var meterNames = await db.Meters.AsNoTracking().Where(m => meterIds.Contains(m.Id)) .ToDictionaryAsync(m => m.Id, m => m.Name); var categoryNames = await db.CostCategories.AsNoTracking().Where(c => categoryIds.Contains(c.Id)) .ToDictionaryAsync(c => c.Id, c => c.Name); return batchIds .Select(id => (Id: id, Targets: new BatchTargets( [.. meterPairs.Where(p => p.Batch == id && meterNames.ContainsKey(p.MeterId)) .Select(p => new MeterTarget(p.MeterId, meterNames[p.MeterId])) .OrderBy(m => m.Name, StringComparer.CurrentCulture)], [.. categoryPairs.Where(p => p.Batch == id && categoryNames.ContainsKey(p.CategoryId)) .Select(p => categoryNames[p.CategoryId]) .Order(StringComparer.CurrentCulture)]))) .Where(t => t.Targets.Meters.Count > 0 || t.Targets.Categories.Count > 0) .ToDictionary(t => t.Id, t => t.Targets); } private sealed record MeterTarget(int Id, string Name); private sealed record BatchTargets(IReadOnlyList Meters, IReadOnlyList Categories); private async Task RevertAsync(ImportBatch batch) { if (!await Confirm.ConfirmAsync(Dialogs, S.Import_RevertConfirmTitle, Loc.F(S.Import_RevertConfirmBody, batch.RowCount, batch.Id, batch.SourceName ?? S.Import_UnnamedSource), S.Import_Revert)) { return; } _reverting = batch.Id; try { await ImportService.RevertAsync(batch.Id); Snackbar.Add(Loc.F(S.Import_BatchReverted, batch.Id), Severity.Success); await LoadBatchesAsync(); } catch (Exception ex) { Snackbar.Add(Loc.F(S.Import_RevertFailed, ex.Message), Severity.Error); } finally { _reverting = null; } } private async Task LoadReferenceAsync() { _loadingReference = true; try { var dir = Path.Combine(AppContext.BaseDirectory, "sampledata"); await ReferenceImporter.LoadAsync(dir); _referenceLoaded = true; Snackbar.Add(S.Import_ReferenceDataLoaded, Severity.Success); await LoadBatchesAsync(); } catch (Exception ex) { Snackbar.Add(Loc.F(S.Import_Failed, ex.Message), Severity.Error); } finally { _loadingReference = false; } } private async Task PreviewAsync(InputFileChangeEventArgs args) { var file = args.File; if (file is null) { return; } var profile = SelectedProfile(); using var reader = new StreamReader(file.OpenReadStream(maxAllowedSize: 5 * 1024 * 1024)); var content = await reader.ReadToEndAsync(); using var stringReader = new StringReader(content); _preview = CsvImporter.Stage(profile, stringReader); } private MappingProfile SelectedProfile() => _profileName switch { "Wasser" => ReferenceProfiles.Water(), "Heizöl" => ReferenceProfiles.HeatingOil(), "Kosten" => ReferenceProfiles.Costs(), _ => ReferenceProfiles.Electricity(), }; }