d5419729e5
- 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
120 lines
4.8 KiB
C#
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 },
|
|
],
|
|
};
|
|
}
|
|
}
|