Files
MeterVault/src/Infrastructure/Import/ReferenceProfiles.cs
T
schmidt.florian d5419729e5 M5: Blazor dashboard (MudBlazor + ApexCharts)
- MudBlazor theme (dark default) + responsive drawer/appbar layout + nav.
- DashboardService read model: KPIs with period-over-period deltas, category breakdown,
  "what cost more/less" difference view, monthly trends.
- Pages: Overview (KPI cards + DeltaChip + donut + difference table), Trends (range-select
  bar chart), Meters (list + source status), Import (load reference dataset + CSV dry-run
  preview), Admin (energy types, tariffs). Charts isolated into components to avoid the
  ApexCharts/MudBlazor Color/Format name clashes.
- ReferenceDataImporter: one-click load of all four sheets as a starter dataset (meters,
  tank, tariff history, category memberships) — bundled sample CSVs copied to app output.
- End-to-end render test: import creates meters + consumption; overview/meters/trends/
  import/admin pages all return 200 with KPI cards rendered.

92 tests green (56 Core + 36 integration).

Deferred to polish: dedicated PV & oil/consumable panels, meter-detail page, full admin
CRUD, prev-year trend overlay.

Claude-Session: https://claude.ai/code/session_01WujdMtMJPbxDpDnMeK22rr
2026-07-13 12:11:12 +02:00

120 lines
4.8 KiB
C#

namespace MeterVault.Infrastructure.Import;
/// <summary>Meter ids the reference profiles map columns to. Defaults are the fixed conventions
/// used by the golden reconciliation tests; the importer supplies real database ids at runtime.</summary>
public sealed record ReferenceMeterIds(
int Haus, int Netz, int Auto, int Solar1, int Solar2, int Wasser, int OilTank, int Burner)
{
public static readonly ReferenceMeterIds Default = new(1, 2, 3, 4, 5, 10, 20, 21);
}
/// <summary>Category ids the Kosten profile maps cost columns to.</summary>
public sealed record ReferenceCategoryIds(int Heizung, int Strom, int Wasser, int Pool)
{
public static readonly ReferenceCategoryIds Default = new(1, 2, 3, 4);
}
/// <summary>
/// Built-in mapping profiles for the four reference <em>Energiebilanz</em> sheets (SDD §6.3).
/// They ship as example imports and as the golden reconciliation fixtures.
/// </summary>
public static class ReferenceProfiles
{
public const int Haus = 1;
public const int Netz = 2;
public const int Auto = 3;
public const int Solar1 = 4;
public const int Solar2 = 5;
public const int Wasser = 10;
public const int OilTank = 20;
public const int Burner = 21;
public const int CategoryHeizung = 1;
public const int CategoryStrom = 2;
public const int CategoryWasser = 3;
public const int CategoryPool = 4;
/// <summary>Linear heating-oil tank calibration: 7000 L / 150 cm ≈ 46.667 L/cm.</summary>
public const double OilLitresPerCm = 7000d / 150d;
public static MappingProfile Electricity(ReferenceMeterIds? ids = null)
{
ids ??= ReferenceMeterIds.Default;
return new MappingProfile
{
Name = "Energiebilanz — Strom",
DateColumn = 0,
DateKind = DateKind.MonthName,
FirstDataRowIndex = 1,
Columns =
[
new ColumnMapping { Index = 1, Role = MappingRole.Reading, MeterId = ids.Haus, Unit = "kWh" },
new ColumnMapping { Index = 2, Role = MappingRole.Reading, MeterId = ids.Netz, Unit = "kWh" },
// Index 3 is a blank spacer column — left unmapped (Ignore).
new ColumnMapping { Index = 4, Role = MappingRole.Reading, MeterId = ids.Auto, Unit = "kWh" },
new ColumnMapping { Index = 5, Role = MappingRole.Reading, MeterId = ids.Solar1, Unit = "kWh" },
new ColumnMapping { Index = 6, Role = MappingRole.Reading, MeterId = ids.Solar2, Unit = "kWh" },
],
};
}
public static MappingProfile Water(ReferenceMeterIds? ids = null)
{
ids ??= ReferenceMeterIds.Default;
return new MappingProfile
{
Name = "Energiebilanz — Wasser",
DateColumn = 0,
DateKind = DateKind.MonthName,
FirstDataRowIndex = 1,
DetectCumulativeSwaps = true,
Columns =
[
new ColumnMapping
{
Index = 1, Role = MappingRole.Reading, MeterId = ids.Wasser, Unit = "m3", SwapConsumptionColumn = 2,
},
],
};
}
public static MappingProfile HeatingOil(ReferenceMeterIds? ids = null)
{
ids ??= ReferenceMeterIds.Default;
return new MappingProfile
{
Name = "Energiebilanz — Heizöl",
DateColumn = 0,
DateKind = DateKind.Auto, // early rows DD.MM.YYYY, later rows month names.
AnchorMonthsToEnd = true, // a monthly snapshot sorts after same-month day-dated readings.
HeaderRowIndex = 3,
FirstDataRowIndex = 4,
Columns =
[
new ColumnMapping { Index = 1, Role = MappingRole.Reading, MeterId = ids.Burner, Unit = "h" },
new ColumnMapping { Index = 4, Role = MappingRole.TankLevel, MeterId = ids.OilTank, Unit = "cm" },
new ColumnMapping { Index = 7, Role = MappingRole.Delivery, MeterId = ids.OilTank, Unit = "L" },
],
};
}
public static MappingProfile Costs(ReferenceCategoryIds? ids = null)
{
ids ??= ReferenceCategoryIds.Default;
return new MappingProfile
{
Name = "Energiebilanz — Kosten",
DateColumn = 0,
DateKind = DateKind.MonthName,
FirstDataRowIndex = 1,
Columns =
[
new ColumnMapping { Index = 3, Role = MappingRole.ManualCost, CategoryId = ids.Heizung },
new ColumnMapping { Index = 4, Role = MappingRole.ManualCost, CategoryId = ids.Strom },
new ColumnMapping { Index = 5, Role = MappingRole.ManualCost, CategoryId = ids.Wasser },
new ColumnMapping { Index = 6, Role = MappingRole.ManualCost, CategoryId = ids.Pool },
],
};
}
}