UI styling. and streamline.

This commit is contained in:
douwe
2026-04-16 23:20:38 +02:00
parent b7726e1974
commit ac7c5e6013
8 changed files with 1232 additions and 203 deletions

BIN
BattSim Logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 84 KiB

View File

@@ -1,3 +1,5 @@
@inherits LayoutComponentBase @inherits LayoutComponentBase
@Body <main>
@Body
</main>

View File

@@ -1,79 +1,543 @@
@page "/" @page "/"
@using Radzen @using Radzen
@using Radzen.Blazor @using Radzen.Blazor
@using BattSim.Models @using BattSim.Models
@using BattSim.Services @using BattSim.Services
<PageTitle>Energie Simulator</PageTitle> <PageTitle>Thuisbatterij Simulator</PageTitle>
<h1>Energie Simulator</h1> <h1 style="color: white">Thuisbatterij Simulator</h1>
<h2>Je Fluvius Energie Data</h2> <div class="pipeline-container">
<div> <!-- Pipeline with 3 connected boxes -->
<p>Ga naar de website van Fluvius en download je historische elektriciteitsverbruiksgegevens met kwartier nauwkeurigheid. Voor een optimale simulatie is het raadzaam om een volledig jaar te downloaden. Dit geeft je inzicht in seizoensgebonden patronen en helpt om realistische uitkomsten te genereren.</p> <div class="pipeline-row">
<InputFile OnChange="OnFileUploaded" accept=".csv"/> <div class="pipeline-connector"></div>
@if (_isLoadingFile) { <p>Data aan het inladen...</p> }
else <!-- Box 1: Data Upload -->
{ <div class="pipeline-box @GetPipelineStepClasses(0)">
@if (FluviusDataRaw.Length != 0) <div class="box-header" @onclick="() => ToggleBox(0)">
{ <div class="step-number">1</div>
<p>@(FluviusDataRaw.Length) kwartieren ingeladen.</p> <div class="step-title">Fluvius Data</div>
} <div class="status-indicator"></div>
</div>
<div class="box-content">
<div class="box-content-inner">
<p class="box-description">Upload je Fluvius kwartierdata om te beginnen</p>
<div class="form-section">
<div class="file-input-wrapper">
<InputFile type="file" id="fileUpload" accept=".csv" onchange="async (e) => await OnFileUploaded(e)" />
<label for="fileUpload" class="file-input-label">
@if (StepData[0].isProcessing)
{
<div class="loading-indicator">
<div class="spinner"></div>
<span style="margin-left: 10px;">Data aan het inladen...</span>
</div>
}
else
{
@if (FluviusDataRaw.Length == 0)
{
<text>Kies CSV bestand</text>
}
else
{
<text>OK @FluviusDataRaw.Length kwartieren geladen</text>
}
}
</label>
@if (FluviusDataRaw.Length > 0 && !StepData[0].isProcessing)
{
<p class="file-name">@_uploadedFileName</p>
}
</div>
</div>
@if (StepData[0].Completed)
{
<div class="success-message">
Data succesvol ingeladen en verwerkt!
</div>
}
@if (_uploadError != null)
{
<div class="error-message">
@_uploadError
</div>
}
<div class="info-text">
<p><strong>Tip:</strong> Download een volledig jaar aan kwartierdata voor de meest accurate resultaten.</p>
</div>
</div>
</div>
</div>
<!-- Box 2: Battery Simulation -->
<div class="pipeline-box @GetPipelineStepClasses(1)">
<div class="box-header" @onclick="() => ToggleBox(1)">
<div class="step-number">2</div>
<div class="step-title">Batterij Simulatie</div>
<div class="status-indicator"></div>
</div>
<div class="box-content">
<div class="box-content-inner">
<p class="box-description">Test verschillende batterijconfiguraties</p>
@if (FluviusDataRaw.Length == 0)
{
<div class="info-text">
<p>Upload eerst je Fluvius data (stap 1) om deze stap te kunnen uitvoeren.</p>
</div>
}
else
{
<div class="form-section">
<div style="display: flex; gap: 1rem;">
<div class="input-group" style="flex: 1;">
<label>Batterijcapaciteit (kWh):</label>
<InputNumber @bind-value="BatteryCapacity" T="double" min="0.1" max="100" step="0.1"/>
</div>
<div class="input-group" style="flex: 1;">
<label>Round-trip Efficiëntie (%):</label>
<InputNumber @bind-value="Efficiency" T="double" min="0" max="100" step="1"/>
</div>
</div>
<p style="font-size: 0.85rem; color: var(--text-clr); margin-top: 1rem;">
Efficiëntie is het percentage van opgeslagen energie dat terug kan worden gebruikt.
</p>
</div>
<div style="display: flex; justify-content: center; margin-top: 1.5rem;">
<button @onclick="SimulateBattery" @onclick:stopPropagation disabled="@(StepData[1].isProcessing || FluviusDataRaw.Length == 0)">
@if (StepData[1].isProcessing)
{
<text>Simuleren...</text>
}
else
{
<text>Simuleer Batterij</text>
}
</button>
</div>
@if (StepData[1].Completed)
{
<div class="success-message">
Simulatie voltooid! @SimulationData.Length kwartieren gesimuleerd.
</div>
}
}
</div>
</div>
</div>
<!-- Box 3: Cost Calculation -->
<div class="pipeline-box @GetPipelineStepClasses(2)">
<div class="box-header" @onclick="() => ToggleBox(2)">
<div class="step-number">3</div>
<div class="step-title">Kosten Berekenen</div>
<div class="status-indicator"></div>
</div>
<div class="box-content">
<div class="box-content-inner">
<p class="box-description">Vergelijk kosten met en zonder batterij</p>
<!-- Vaste Kosten -->
<div style="margin-bottom: 1.5rem;">
<h4 style="color: var(--green-clr); margin-bottom: 0.75rem; border-bottom: 1px solid var(--border-clr); padding-bottom: 0.5rem;">
Vaste Kosten
</h4>
<div style="display: flex; gap: 1rem; flex-wrap: wrap;">
<div class="input-group" style="flex: 1;">
<label>Vaste vergoeding (€/jaar):</label>
<InputNumber @bind-value="calculator.BasePayment" T="double" min="0" step="1"/>
</div>
<div class="input-group" style=" flex: 1;">
<label>Databeheer tarief (€/jaar):</label>
<InputNumber @bind-value="calculator.DataManagementCost" T="double" min="0" step="0.01"/>
</div>
<div class="input-group" style=" flex: 1;">
<label>Vlaamse energieheffing (€/jaar):</label>
<InputNumber @bind-value="calculator.FlemishEnergyTariff" T="double" min="0" step="0.01"/>
</div>
</div>
</div>
<!-- Energiekosten -->
<div style="margin-bottom: 1.5rem;">
<h4 style="color: var(--green-clr); margin-bottom: 0.75rem; border-bottom: 1px solid var(--border-clr); padding-bottom: 0.5rem;">
Energiekosten
</h4>
<div style="display: flex; gap: 1rem; flex-wrap: wrap;">
<div class="input-group" style=" flex: 1;">
<label>Energiekost (c€/kWh):</label>
<InputNumber @bind-value="calculator.EnergyCost" T="double" min="0" step="0.0001"/>
</div>
<div class="input-group" style=" flex: 1;">
<label>Terugleveringsvergoeding (c€/kWh):</label>
<InputNumber @bind-value="calculator.ReturnCost" T="double" min="0" step="0.0001"/>
</div>
</div>
</div>
<!-- Heffingen Groene Stroom -->
<div style="margin-bottom: 1.5rem;">
<h4 style="color: var(--green-clr); margin-bottom: 0.75rem; border-bottom: 1px solid var(--border-clr); padding-bottom: 0.5rem;">
Heffingen Groene Stroom
</h4>
<div style="display: flex; gap: 1rem; flex-wrap: wrap;">
<div class="input-group" style=" flex: 1;">
<label>Groene stroomcertificaten (c€/kWh):</label>
<InputNumber @bind-value="calculator.GreenCertificateCost" T="double" min="0" step="0.0001"/>
</div>
<div class="input-group" style=" flex: 1;">
<label>Warmtekracht certificaten (c€/kWh):</label>
<InputNumber @bind-value="calculator.HeatingCertificateCost" T="double" min="0" step="0.0001"/>
</div>
</div>
</div>
<!-- Net- en distributiekosten -->
<div style="margin-bottom: 1.5rem;">
<h4 style="color: var(--green-clr); margin-bottom: 0.75rem; border-bottom: 1px solid var(--border-clr); padding-bottom: 0.5rem;">
Net- en distributiekosten
</h4>
<div style="display: flex; gap: 1rem; flex-wrap: wrap;">
<div class="input-group" style=" flex: 1;">
<label>Capaciteitstarief (€/kWh/jaar):</label>
<InputNumber @bind-value="calculator.CapacityCost" T="double" min="0" step="0.01"/>
</div>
<div class="input-group" style=" flex: 1;">
<label>Afnametarief (c€/kWh):</label>
<InputNumber @bind-value="calculator.UsageTariff" T="double" min="0" step="0.0001"/>
</div>
</div>
</div>
<!-- Heffingen en Toeslagen -->
<div style="margin-bottom: 1.5rem;">
<h4 style="color: var(--green-clr); margin-bottom: 0.75rem; border-bottom: 1px solid var(--border-clr); padding-bottom: 0.5rem;">
Heffingen en Toeslagen
</h4>
<div style="display: flex; gap: 1rem; flex-wrap: wrap;">
<div class="input-group" style=" flex: 1;">
<label>Energiebijdrage (c€/kWh):</label>
<InputNumber @bind-value="calculator.EnergyContribution" T="double" min="0" step="0.0001"/>
</div>
<div class="input-group" style=" flex: 1;">
<label>Bijzondere accijns (c€/kWh):</label>
<InputNumber @bind-value="calculator.SpecialTariffs" T="double" min="0" step="0.0001"/>
</div>
</div>
</div>
<div style="display: flex; justify-content: center; margin-top: 1.5rem;">
<button @onclick="Calculate" @onclick:stopPropagation disabled="@(StepData[2].isProcessing || SimulationData.Length == 0)">
@if (StepData[2].isProcessing)
{
<text>Berekenen...</text>
}
else
{
<text>Bereken Kosten</text>
}
</button>
</div>
@if (StepData[2].Completed)
{
<div class="results">
<p><strong>Originele kosten:</strong> <span class="result-value">€@normalCost.ToString("0.00")</span></p>
<p><strong>Gesimuleerde kosten:</strong> <span class="result-value">€@simulatedCost.ToString("0.00")</span></p>
<p><strong>Besparing:</strong> <span class="result-value">€@((normalCost - simulatedCost).ToString("0.00"))</span></p>
@if (normalCost > 0)
{
<p><strong>Besparingspercent:</strong> <span class="result-value">@(((normalCost - simulatedCost) / normalCost * 100).ToString("0.00"))%</span></p>
}
</div>
}
</div>
</div>
</div>
</div>
@if (FluviusDataDaily.Length != 0)
{
<RadzenChart>
<RadzenAreaSeries Smooth=true Data="@FluviusDataDaily" CategoryProperty="Time" Title="Consumption" ValueProperty="Consumption">
<RadzenChartTooltipOptions Visible="true"/>
</RadzenAreaSeries>
<RadzenAreaSeries Smooth=true Data="@FluviusDataDaily" CategoryProperty="Time" Title="Production" ValueProperty="Production">
<RadzenChartTooltipOptions Visible="true"/>
</RadzenAreaSeries>
<RadzenCategoryAxis Formatter="@FormatObject" Padding="20" LabelAutoRotation="-45">
<RadzenGridLines Visible="true"/>
<RadzenAxisTitle Text="Time"/>
</RadzenCategoryAxis>
<RadzenValueAxis Formatter="@FormatObject">
<RadzenGridLines Visible="true"/>
<RadzenAxisTitle Text="Energy"/>
</RadzenValueAxis>
</RadzenChart>
}
}
</div> </div>
@code{
<!-- Chart - Directly on page, full width -->
@if (FluviusDataDaily.Length > 0)
{
<div class="chart-container">
<h2>Overzicht: Energie Data & Simulatie</h2>
<!-- Time Period Toggle -->
<div class="time-toggle-container">
<button class="@GetTimeButtonClass("all")" @onclick="SetTimePeriodAll" @onclick:stopPropagation>
All periodes
</button>
<button class="@GetTimeButtonClass("year")" @onclick="SetTimePeriodYear" @onclick:stopPropagation>
Per jaar
</button>
<button class="@GetTimeButtonClass("month")" @onclick="SetTimePeriodMonth" @onclick:stopPropagation>
Per maand
</button>
<button class="@GetTimeButtonClass("week")" @onclick="SetTimePeriodWeek" @onclick:stopPropagation>
Per week
</button>
<button class="@GetTimeButtonClass("day")" @onclick="SetTimePeriodDay" @onclick:stopPropagation>
Per dag
</button>
</div>
<RadzenChart Class="unified-chart">
<!-- Original Data -->
<RadzenAreaSeries Smooth=true Data="@FilteredFluviusData" CategoryProperty="Time" Title="Consumption" ValueProperty="Consumption">
</RadzenAreaSeries>
<RadzenAreaSeries Smooth=true Data="@FilteredFluviusData" CategoryProperty="Time" Title="Production" ValueProperty="Production">
</RadzenAreaSeries>
<!-- Simulated Data -->
@if (SimulationDataDaily.Length > 0)
{
<RadzenAreaSeries Smooth=true Data="@FilteredSimulationData" CategoryProperty="Time" Title="Simulated Consumption" ValueProperty="Consumption">
</RadzenAreaSeries>
<RadzenAreaSeries Smooth=true Data="@FilteredSimulationData" CategoryProperty="Time" Title="Simulated Production" ValueProperty="Production">
</RadzenAreaSeries>
<RadzenAreaSeries Smooth=true Data="@FilteredSimulationData" CategoryProperty="Time" Title="Battery Charge" ValueProperty="BatteryCharge">
</RadzenAreaSeries>
}
<RadzenCategoryAxis Formatter="@FormatObject" Padding="20" LabelAutoRotation="-45">
<RadzenGridLines Visible="true"/>
<RadzenAxisTitle Text="Tijd"/>
</RadzenCategoryAxis>
<RadzenValueAxis Formatter="@FormatObject">
<RadzenGridLines Visible="true"/>
<RadzenAxisTitle Text="Energie (kWh)"/>
</RadzenValueAxis>
</RadzenChart>
</div>
}
@code {
EnergyData[] FluviusDataRaw = []; EnergyData[] FluviusDataRaw = [];
EnergyData[] FluviusDataDaily = []; EnergyData[] FluviusDataDaily = [];
bool _isLoadingFile = false;
SimulatedBatteryEnergyData[] SimulationData = [];
SimulatedBatteryEnergyData[] SimulationDataDaily = [];
EnergyData[] FilteredFluviusData = [];
SimulatedBatteryEnergyData[] FilteredSimulationData = [];
EnergyCostCalculator calculator = new();
double normalCost = 0.0;
double simulatedCost = 0.0;
struct PipelineStep{
public bool Expanded;
public bool Completed;
public bool isProcessing;
}
PipelineStep[] StepData = [
new PipelineStep(), // Step 1
new PipelineStep(), // Step 2
new PipelineStep(), // Step 3
];
string _uploadedFileName = string.Empty;
string? _uploadError = null;
string _timePeriod = "all";
double BatteryCapacity = 7.5;
double Efficiency = 90;
private string GetPipelineStepClasses(int step){
var classes = new List<string>();
if (StepData[step].Expanded) classes.Add("expanded");
if (StepData[step].Completed) classes.Add("completed");
if (StepData[step].isProcessing) classes.Add("running");
return string.Join(" ", classes);
}
private string GetTimeButtonClass(string period)
{
var baseClass = "time-toggle-button";
if (_timePeriod == period)
{
return $"{baseClass} active";
}
return baseClass;
}
private void SetTimePeriodAll() => SetTimePeriod("all");
private void SetTimePeriodYear() => SetTimePeriod("year");
private void SetTimePeriodMonth() => SetTimePeriod("month");
private void SetTimePeriodWeek() => SetTimePeriod("week");
private void SetTimePeriodDay() => SetTimePeriod("day");
private void ToggleBox(int step)
{
if(step == 1 && FluviusDataRaw.Length == 0) return;
if(step == 2 && SimulationData.Length == 0) return;
StepData[step].Expanded = !StepData[step].Expanded;
StateHasChanged();
}
private async Task OnFileUploaded(InputFileChangeEventArgs e) private async Task OnFileUploaded(InputFileChangeEventArgs e)
{ {
_uploadError = null;
var file = e.File; var file = e.File;
_uploadedFileName = file.Name;
if (file.ContentType != "text/csv") if (file.ContentType != "text/csv")
{ {
Console.WriteLine("Only CSV files are allowed!"); _uploadError = "Alleen CSV bestanden zijn toegestaan!";
StateHasChanged();
return; return;
} }
try try
{ {
Console.WriteLine("Reading csv file..."); StepData[0].isProcessing = true;
_isLoadingFile = true;
StateHasChanged(); StateHasChanged();
FluviusDataRaw = await FluviusDataHandler.LoadAndProcessFile(file); FluviusDataRaw = await FluviusDataHandler.LoadAndProcessFile(file);
FluviusDataDaily = FluviusDataHandler.GenerateDailyData(FluviusDataRaw); FluviusDataDaily = FluviusDataHandler.GenerateDailyData(FluviusDataRaw);
_isLoadingFile = false; FilteredFluviusData = FluviusDataDaily;
FilteredSimulationData = [];
StepData[0].isProcessing = false;
StepData[0].Completed = true;
StepData[1].Completed = false;
StepData[2].Completed = false;
SimulationData = [];
SimulationDataDaily = [];
StateHasChanged(); StateHasChanged();
Console.WriteLine("Done reading csv file!");
} }
catch (Exception ex) catch (Exception ex)
{ {
Console.WriteLine($"Error loading file: {ex.Message}"); StepData[0].isProcessing = false;
_uploadError = ex.Message;
StateHasChanged();
} }
} }
private void OnSeriesClick(){} private void SimulateBattery()
{
if (FluviusDataRaw.Length == 0) return;
StepData[1].isProcessing = true;
StateHasChanged();
try
{
SimulationData = BatterySimulator.SimulateBattery(FluviusDataRaw, BatteryCapacity, Efficiency/100);
SimulationDataDaily = BatterySimulator.GenerateDailyData(SimulationData);
FilteredSimulationData = SimulationDataDaily;
StepData[1].isProcessing = false;
StepData[1].Completed = true;
StepData[2].Completed = false;
normalCost = 0;
simulatedCost = 0;
StateHasChanged();
}
catch (Exception ex)
{
StepData[1].isProcessing = false;
Console.WriteLine($"Error: {ex.Message}");
}
}
private void Calculate()
{
if (SimulationData.Length == 0) return;
StepData[2].isProcessing = true;
StateHasChanged();
try
{
normalCost = calculator.CalculateCostOfEnergyUsage(FluviusDataRaw);
simulatedCost = calculator.CalculateCostOfEnergyUsage(SimulationData);
StepData[2].isProcessing = false;
StepData[2].Completed = true;
StateHasChanged();
}
catch (Exception ex)
{
StepData[2].isProcessing = false;
Console.WriteLine($"Error: {ex.Message}");
}
}
private void SetTimePeriod(string period)
{
_timePeriod = period;
// Filter data based on selected period
FilteredFluviusData = FilterFluviusDataByPeriod(period);
FilteredSimulationData = FilterSimulationDataByPeriod(period);
StateHasChanged();
}
private EnergyData[] FilterFluviusDataByPeriod(string period)
{
if (period == "all" || FluviusDataDaily.Length == 0)
return FluviusDataDaily;
var result = new List<EnergyData>();
foreach (var item in FluviusDataDaily)
{
// The Time property is DateTime but for daily data it represents the day
var date = DateOnly.FromDateTime(item.Time);
bool include = false;
switch (period)
{
case "day": include = true; break; // Show all daily data
case "week": include = date.DayOfWeek == DayOfWeek.Monday; break;
case "month": include = date.Day == 1; break;
case "year": include = date.Month == 1 && date.Day == 1; break;
}
if (include) result.Add(item);
}
return result.ToArray();
}
private SimulatedBatteryEnergyData[] FilterSimulationDataByPeriod(string period)
{
if (period == "all" || SimulationDataDaily.Length == 0)
return SimulationDataDaily;
var result = new List<SimulatedBatteryEnergyData>();
foreach (var item in SimulationDataDaily)
{
var date = DateOnly.FromDateTime(item.Time);
bool include = false;
switch (period)
{
case "day": include = true; break;
case "week": include = date.DayOfWeek == DayOfWeek.Monday; break;
case "month": include = date.Day == 1; break;
case "year": include = date.Month == 1 && date.Day == 1; break;
}
if (include) result.Add(item);
}
return result.ToArray();
}
private string FormatObject(object value) { private string FormatObject(object value) {
if(value is double d) return $"{d:0.##} kWh"; if(value is double d) return $"{d:0.##} kWh";
if(value is DateTime time) return time.ToString("dd/MM/yyyy"); if(value is DateTime time) return time.ToString("dd/MM/yyyy");
@@ -81,156 +545,3 @@
return string.Empty; return string.Empty;
} }
} }
<h2>Batterij Simulatie</h2>
<div>
<p>
Met je Fluvius-kwartierdata simuleren we een batterij die elk kwartier de energie die normaal op het elektriciteitsnet wordt teruggeleverd, opvangt en de stroom die normaal uit het net wordt gehaald vervangt—mits de batterij nog voldoende capaciteit heeft.
Op deze manier kun je verschillende batterijcapaciteiten testen om te zien welke installatie het beste past bij jouw verbruiksprofiel en besparingsdoelen. Daarnaast kun je onderzoeken hoe verschillende batterijtypes, met uiteenlopende efficiëntieniveaus, de uitkomsten beïnvloeden. Onder efficiëntie verstaan we hier het aandeel van de opgeslagen energie dat uiteindelijk ook daadwerkelijk weer kan worden gebruikt.
</p>
<ul>
<li>
<p>Batterijcapaciteit: <InputNumber @bind-value="BatteryCapacity"/> kWh</p>
</li>
<li>
<p>Round-trip Efficiëntie: <InputNumber @bind-value="Efficiency"/> %</p>
</li>
</ul>
<Button @onclick="SimulateBattery">Simulate</Button>
@if (_isSimulating) { <p>Simulating...</p> }
else
{
@if (SimulationDataDaily.Length != 0)
{
<RadzenChart>
<RadzenAreaSeries Smooth=true Data="@FluviusDataDaily" CategoryProperty="Time" Title="Consumption" ValueProperty="Consumption">
<RadzenChartTooltipOptions Visible="true"/>
</RadzenAreaSeries>
<RadzenAreaSeries Smooth=true Data="@FluviusDataDaily" CategoryProperty="Time" Title="Production" ValueProperty="Production">
<RadzenChartTooltipOptions Visible="true"/>
</RadzenAreaSeries>
<RadzenAreaSeries Smooth=true Data="@SimulationDataDaily" CategoryProperty="Time" Title="SimulatedConsumption" ValueProperty="Consumption">
<RadzenChartTooltipOptions Visible="true"/>
</RadzenAreaSeries>
<RadzenAreaSeries Smooth=true Data="@SimulationDataDaily" CategoryProperty="Time" Title="SimulatedProduction" ValueProperty="Production">
<RadzenChartTooltipOptions Visible="true"/>
</RadzenAreaSeries>
<RadzenAreaSeries Smooth=true Data="@SimulationDataDaily" CategoryProperty="Time" Title="BatteryCharge" ValueProperty="BatteryCharge">
<RadzenChartTooltipOptions Visible="true"/>
</RadzenAreaSeries>
<RadzenCategoryAxis Formatter="@FormatObject" Padding="20" LabelAutoRotation="-45">
<RadzenGridLines Visible="true"/>
<RadzenAxisTitle Text="Time"/>
</RadzenCategoryAxis>
<RadzenValueAxis Formatter="@FormatObject">
<RadzenGridLines Visible="true"/>
<RadzenAxisTitle Text="Energy"/>
</RadzenValueAxis>
</RadzenChart>
}
}
</div>
@code {
double BatteryCapacity = 7.5;
double Efficiency = 90;
bool _isSimulating = false;
SimulatedBatteryEnergyData[] SimulationData = [];
SimulatedBatteryEnergyData[] SimulationDataDaily = [];
private void SimulateBattery(){
Console.WriteLine("Simulating...");
_isSimulating = true;
SimulationData = BatterySimulator.SimulateBattery(FluviusDataRaw, BatteryCapacity, Efficiency/100);
SimulationDataDaily = BatterySimulator.GenerateDailyData(SimulationData);
_isSimulating = false;
Console.WriteLine("Done simulating!");
}
}
<h2>Bereken kosten</h2>
<div>
<p>
Uitleg
</p>
<p>Vaste Vergoeding</p>
<ul>
<li>
<p>Vaste vergoeding: <InputNumber @bind-value="calculator.BasePayment" /> €/jaar</p>
</li>
</ul>
<p>Energiekosten</p>
<ul>
<li>
<p>Energiekost: <InputNumber @bind-value="calculator.EnergyCost" /> c€/kWh</p>
</li>
<li>
<p>Terugleveringsvergoeding: <InputNumber @bind-value="calculator.ReturnCost" /> c€/kWh</p>
</li>
</ul>
<p>Heffingen Groene Stroom</p>
<ul>
<li>
<p>Groene stroomcertificaten: <InputNumber @bind-value="calculator.GreenCertificateCost" /> c€/kWh</p>
</li>
<li>
<p>Warmtekracht certificaten: <InputNumber @bind-value="calculator.HeatingCertificateCost" /> c€/kWh</p>
</li>
</ul>
<p>Net- en distributiekosten</p>
<ul>
<li>
<p>Tarief databeheer: <InputNumber @bind-value="calculator.DataManagementCost" /> €/jaar</p>
</li>
<li>
<p>Capaciteitstarief: <InputNumber @bind-value="calculator.CapacityCost" /> €/kWh/jaar</p>
</li>
<li>
<p>Afnametarief: <InputNumber @bind-value="calculator.UsageTariff" /> c€/kWh</p>
</li>
</ul>
<p>Heffingen en Toeslagen</p>
<ul>
<li>
<p>Energiebijdrage: <InputNumber @bind-value="calculator.EnergyContribution" /> c€/kWh</p>
</li>
<li>
<p>Bijzondere accijns elektriciteit: <InputNumber @bind-value="calculator.SpecialTariffs" /> c€/kWh</p>
</li>
<li>
<p>Vlaamse energieheffing elektriciteit: <InputNumber @bind-value="calculator.FlemishEnergyTariff" /> €/jaar</p>
</li>
</ul>
<Button @onclick="Calculate">Calculate</Button>
@if (_isCalculating) { <p>Calculating...</p> }
else
{
<p>The original price is: €@normalCost</p>
<p>The simulated price is: €@simulatedCost</p>
<p>The costsaving is: €@(normalCost - simulatedCost)</p>
}
</div>
@code {
EnergyCostCalculator calculator = new();
double normalCost = 0.0;
double simulatedCost = 0.0;
bool _isCalculating = false;
private void Calculate()
{
Console.WriteLine("Calculating...");
_isCalculating = true;
normalCost = calculator.CalculateCostOfEnergyUsage(FluviusDataRaw);
simulatedCost = calculator.CalculateCostOfEnergyUsage(SimulationData);
_isCalculating = false;
Console.WriteLine("Done Calculating!");
}
}

View File

@@ -85,7 +85,7 @@ namespace BattSim.Services
await reader.ReadLineAsync(); await reader.ReadLineAsync();
// Read all lines into memory // Read all lines into memory
var lines = new List<string>(); var lines = new List<string>();
string line; string? line;
while ((line = await reader.ReadLineAsync()) is not null) while ((line = await reader.ReadLineAsync()) is not null)
{ {
lines.Add(line); lines.Add(line);

708
wwwroot/css/site.css Normal file
View File

@@ -0,0 +1,708 @@
/* ===== Douwco.be Theme ===== */
@font-face {
font-family: 'Righteous';
src: url('/fonts/Righteous.ttf') format('truetype');
}
@font-face {
font-family: 'Montserrat';
src: url('/fonts/Montserrat.ttf') format('truetype');
font-weight: normal;
}
@font-face {
font-family: 'Montserrat-Bold';
src: url('/fonts/Montserrat.ttf') format('truetype');
font-weight: bold;
}
:root {
--background-clr: #283e3e;
--background-accent-clr: #324f4f;
--blue-clr: #47bcdf;
--green-clr: #6ede9a;
--purple-clr: #a48da;
--orange-clr: #e2a661;
--white-clr: #ffffff;
--text-clr: #e0e0e0;
--border-clr: #47bcdf;
--success-clr: #2ecc71;
--warning-clr: #f39c12;
--error-clr: #e74c3c;
}
/* ===== Base Styles ===== */
html, body {
height: 100%;
margin: 0;
padding: 0;
font-family: 'Montserrat', sans-serif;
}
body {
background-color: var(--background-clr);
color: var(--text-clr);
display: flex;
flex-direction: column;
min-height: 100vh;
}
main {
flex: 1;
margin: 20px;
overflow-y: auto;
overflow-x: hidden;
scroll-behavior: smooth;
}
h1, h2, h3, h4, h5, h6 {
font-family: 'Righteous', sans-serif;
color: var(--white-clr);
}
h1 {
font-size: 2.5rem;
margin-bottom: 2rem;
text-align: center;
}
h2 {
font-size: 1.8rem;
margin: 1.5rem 0 1rem 0;
color: var(--blue-clr);
}
h3 {
font-size: 1.3rem;
margin: 1rem 0 0.5rem 0;
color: var(--green-clr);
}
p {
line-height: 1.6;
margin: 0.5rem 0;
}
a {
color: var(--blue-clr);
text-decoration: none;
}
a:hover {
color: var(--green-clr);
}
/* ===== Pipeline Layout ===== */
.pipeline-container {
display: flex;
flex-direction: column;
gap: 2rem;
margin: 2rem 0;
}
.pipeline-row {
display: flex;
justify-content: center;
align-items: stretch;
gap: 1rem;
position: relative;
margin-bottom: 2rem;
}
.pipeline-connector {
position: absolute;
top: 25px;
left: 0;
right: 0;
height: 4px;
background: linear-gradient(to right, var(--blue-clr), var(--green-clr));
z-index: 0;
}
.pipeline-connector::before,
.pipeline-connector::after {
content: '';
position: absolute;
width: 12px;
height: 12px;
border-radius: 50%;
background: var(--blue-clr);
top: -4px;
}
.pipeline-connector::before {
left: -6px;
}
.pipeline-connector::after {
right: -6px;
background: var(--green-clr);
}
/* ===== Pipeline Boxes ===== */
.pipeline-box {
background-color: var(--background-accent-clr);
border-radius: 15px;
padding: 1.5rem;
width: 300px;
position: relative;
z-index: 1;
transition: border-color 0.3s, box-shadow 0.3s;
border: 2px solid var(--background-accent-clr);
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.3);
height: 100px;
overflow: hidden;
}
.pipeline-box:hover {
border-color: var(--blue-clr);
box-shadow: 0 6px 20px rgba(71, 188, 223, 0.3);
}
.pipeline-box.expanded {
width: 360px;
border-color: var(--green-clr);
box-shadow: 0 8px 25px rgba(110, 222, 154, 0.4);
height: auto;
overflow: visible;
}
.pipeline-box:not(.expanded) .box-content {
display: none;
}
.pipeline-box.completed {
border-color: var(--success-clr);
}
.pipeline-box.completed .box-header {
border-bottom-color: var(--success-clr);
}
.pipeline-box.completed .status-indicator {
background-color: var(--success-clr);
box-shadow: 0 0 15px var(--success-clr);
}
.pipeline-box.completed .step-number {
background-color: var(--success-clr);
}
/* Box Header */
.pipeline-box .box-header {
display: flex;
cursor: pointer;
justify-content: space-between;
align-items: center;
margin-bottom: 1rem;
padding-bottom: 1rem;
border-bottom: 2px solid var(--border-clr);
transition: all 0.3s ease;
}
.pipeline-box.expanded .box-header {
border-bottom-color: var(--green-clr);
}
.pipeline-box .step-number {
width: 40px;
height: 40px;
border-radius: 50%;
background: linear-gradient(135deg, var(--blue-clr), var(--green-clr));
display: flex;
align-items: center;
justify-content: center;
font-family: 'Montserrat-Bold', sans-serif;
font-size: 1.2rem;
color: white;
font-weight: bold;
}
.pipeline-box .step-title {
font-size: 1.3rem;
font-family: 'Montserrat-Bold', sans-serif;
color: var(--white-clr);
flex: 1;
text-align: center;
}
.pipeline-box .status-indicator {
width: 12px;
height: 12px;
border-radius: 50%;
background-color: var(--background-clr);
border: 2px solid var(--blue-clr);
transition: all 0.3s ease;
}
.pipeline-box.running .status-indicator {
background-color: var(--orange-clr);
animation: pulse 1.5s infinite;
}
/* Box Content */
.pipeline-box .box-content {
max-height: 0;
overflow: hidden;
transition: max-height 0.5s cubic-bezier(0.68, -0.55, 0.265, 1.55);
}
.pipeline-box.expanded .box-content {
max-height: 3000px;
}
.pipeline-box:not(.expanded) .box-content {
display: none;
}
.pipeline-box .box-content-inner {
padding: 0.5rem 0;
}
.pipeline-box .box-description {
font-size: 0.95rem;
color: var(--text-clr);
margin-bottom: 1.5rem;
text-align: center;
}
/* Form Elements */
.pipeline-box .form-section {
margin: 1rem 0;
}
.pipeline-box label {
display: block;
margin-bottom: 0.5rem;
font-weight: 600;
color: var(--text-clr);
}
.pipeline-box .input-group {
margin-bottom: 1rem;
}
.pipeline-box .input-group input[type="number"] {
margin-bottom: 1rem;
}
.pipeline-box input[type="number"],
.pipeline-box input[type="file"] {
padding: 0.75rem;
border: 2px solid var(--background-clr);
border-radius: 8px;
background-color: var(--background-clr);
color: var(--white-clr);
font-family: 'Montserrat', sans-serif;
font-size: 1rem;
transition: all 0.3s ease;
}
.pipeline-box input[type="number"]:focus,
.pipeline-box input[type="file"]:focus {
outline: none;
border-color: var(--blue-clr);
box-shadow: 0 0 10px rgba(71, 188, 223, 0.5);
}
.pipeline-box input[type="number"] {
-moz-appearance: textfield;
width: 75px;
}
.pipeline-box input[type="number"]::-webkit-outer-spin-button,
.pipeline-box input[type="number"]::-webkit-inner-spin-button {
-webkit-appearance: none;
margin: 0;
}
.pipeline-box input[type="file"] {
width: 100%;
}
.pipeline-box .file-input-wrapper {
border: 2px dashed var(--border-clr);
border-radius: 8px;
padding: 1.5rem;
text-align: center;
transition: all 0.3s ease;
}
.pipeline-box .file-input-wrapper:hover {
border-color: var(--blue-clr);
}
.pipeline-box .file-input-wrapper input[type="file"] {
display: none;
}
.pipeline-box .file-input-label {
color: var(--blue-clr);
cursor: pointer;
font-weight: 600;
}
.pipeline-box .file-name {
margin-top: 0.5rem;
font-size: 0.9rem;
color: var(--green-clr);
word-break: break-all;
}
/* Buttons */
.pipeline-box button,
.btn {
display: inline-block;
padding: 0.75rem 1.5rem;
border: none;
border-radius: 8px;
background: linear-gradient(135deg, var(--blue-clr), var(--green-clr));
color: white;
font-family: 'Montserrat-Bold', sans-serif;
font-size: 1rem;
cursor: pointer;
transition: all 0.3s ease;
text-transform: uppercase;
letter-spacing: 1px;
margin-top: 1rem;
}
.pipeline-box button:hover,
.btn:hover {
transform: translateY(-2px);
box-shadow: 0 5px 15px rgba(71, 188, 223, 0.4);
}
.pipeline-box button:active,
.btn:active {
transform: translateY(0);
}
.pipeline-box button:disabled,
.btn:disabled {
opacity: 0.6;
cursor: not-allowed;
transform: none;
}
.pipeline-box.button-container {
display: flex;
justify-content: center;
margin-top: 1rem;
}
/* Results Display */
.pipeline-box .results {
margin-top: 1.5rem;
padding: 1rem;
background-color: rgba(71, 188, 223, 0.1);
border-radius: 8px;
border-left: 4px solid var(--blue-clr);
}
.pipeline-box .results p {
margin: 0.5rem 0;
font-size: 1rem;
}
.pipeline-box .results .result-value {
font-family: 'Montserrat-Bold', sans-serif;
color: var(--green-clr);
font-size: 1.2rem;
}
/* ===== Chart Display ===== */
.chart-container {
width: 100%;
margin: 2rem 0 0 0;
padding: 0;
}
.chart-container h2 {
text-align: center;
margin-bottom: 1.5rem;
color: var(--white-clr);
}
.unified-chart {
min-height: 500px;
background-color: var(--background-accent-clr);
border-radius: 15px;
padding: 1.5rem;
margin: 0 auto;
max-width: calc(100vw - 4rem);
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.3);
border: 1px solid var(--border-clr);
}
/* Time Period Toggle */
.time-toggle-container {
display: flex;
justify-content: center;
gap: 0.5rem;
margin-bottom: 1rem;
flex-wrap: wrap;
}
.time-toggle-button {
padding: 0.5rem 1rem;
border: 2px solid var(--border-clr);
background-color: var(--background-accent-clr);
color: var(--text-clr);
border-radius: 8px;
cursor: pointer;
transition: all 0.3s ease;
font-family: 'Montserrat', sans-serif;
font-size: 0.9rem;
}
.time-toggle-button:hover {
border-color: var(--blue-clr);
background-color: rgba(71, 188, 223, 0.1);
}
.time-toggle-button.active {
background: linear-gradient(135deg, var(--blue-clr), var(--green-clr));
color: white;
border-color: transparent;
box-shadow: 0 4px 12px rgba(71, 188, 223, 0.4);
}
.time-toggle-button.active:hover {
transform: translateY(-2px);
}
/* For active button state via data attribute */
.time-toggle-button[data-active="true"] {
background: linear-gradient(135deg, var(--blue-clr), var(--green-clr));
color: white;
border-color: transparent;
box-shadow: 0 4px 12px rgba(71, 188, 223, 0.4);
}
.time-toggle-button[data-active="true"]:hover {
transform: translateY(-2px);
}
/* ===== Loading States ===== */
.loading-indicator {
display: flex;
align-items: center;
justify-content: center;
padding: 1rem;
}
.spinner {
width: 40px;
height: 40px;
border: 4px solid var(--background-clr);
border-top-color: var(--blue-clr);
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
@keyframes pulse {
0%, 100% { opacity: 1; transform: scale(1); }
50% { opacity: 0.5; transform: scale(1.2); }
}
/* ===== Chart Styling ===== */
.chart-container {
background-color: var(--background-clr);
border-radius: 8px;
padding: 1rem;
margin: 1rem 0;
}
/* Radzen Chart Customization */
:root {
--rz-color-primary: var(--blue-clr);
--rz-color-secondary: var(--green-clr);
--rz-color-text: var(--white-clr);
--rz-color-surface: var(--background-accent-clr);
--rz-color-border: var(--border-clr);
}
.rz-chart {
background-color: var(--background-clr) !important;
}
.rz-chart text {
fill: var(--white-clr) !important;
font-family: 'Montserrat', sans-serif !important;
}
.rz-series-0 path {
stroke: var(--blue-clr) !important;
fill: rgba(71, 188, 223, 0.3) !important;
}
.rz-series-1 path {
stroke: var(--green-clr) !important;
fill: rgba(110, 222, 154, 0.3) !important;
}
.rz-series-2 path {
stroke: var(--purple-clr) !important;
fill: rgba(164, 141, 170, 0.3) !important;
}
.rz-series-3 path {
stroke: var(--orange-clr) !important;
fill: rgba(226, 166, 97, 0.3) !important;
}
.rz-series-4 path {
stroke: #e74c3c !important;
fill: rgba(231, 76, 60, 0.3) !important;
}
.rz-gridline line {
stroke: var(--background-clr) !important;
}
/* ===== Cost Calculation Grid ===== */
.cost-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 1rem;
margin: 1rem 0;
}
.cost-grid .input-group {
margin-bottom: 0.5rem;
}
.cost-grid label {
font-size: 0.85rem;
}
.cost-grid input {
font-size: 0.85rem;
padding: 0.5rem;
}
/* ===== Responsive Design ===== */
@media (max-width: 1200px) {
.pipeline-row {
flex-wrap: wrap;
justify-content: center;
}
.pipeline-connector {
display: none;
}
.pipeline-box {
width: 280px;
}
.pipeline-box.expanded {
width: 340px;
}
.unified-chart-container {
width: calc(100% - 2rem);
}
}
@media (max-width: 900px) {
.pipeline-box {
width: 100%;
min-height: 150px;
}
.pipeline-box.expanded {
width: 100%;
}
.pipeline-row {
flex-direction: column;
align-items: center;
}
.pipeline-connector {
display: none;
}
.unified-chart-container {
width: calc(100% - 2rem);
margin: 1rem;
}
}
@media (max-width: 768px) {
main {
margin: 10px;
}
h1 {
font-size: 1.8rem;
}
h2 {
font-size: 1.4rem;
}
.pipeline-box {
min-width: 100%;
}
.pipeline-box.expanded {
min-width: 100%;
transform: none;
}
.unified-chart-container {
padding: 1rem;
margin: 1rem;
}
}
/* ===== Info Text ===== */
.info-text {
background-color: rgba(71, 188, 223, 0.1);
border-left: 4px solid var(--blue-clr);
padding: 1rem;
border-radius: 8px;
margin: 1rem 0;
font-size: 0.95rem;
}
.info-text p {
margin: 0;
}
/* ===== Success Message ===== */
.success-message {
background-color: rgba(46, 204, 113, 0.2);
border-left: 4px solid var(--success-clr);
padding: 1rem;
border-radius: 8px;
margin: 1rem 0;
font-size: 1rem;
color: var(--success-clr);
}
/* ===== Error Message ===== */
.error-message {
background-color: rgba(231, 76, 60, 0.2);
border-left: 4px solid var(--error-clr);
padding: 1rem;
border-radius: 8px;
margin: 1rem 0;
font-size: 1rem;
color: var(--error-clr);
}

Binary file not shown.

BIN
wwwroot/fonts/Righteous.ttf Normal file

Binary file not shown.

View File

@@ -4,16 +4,23 @@
<head> <head>
<meta charset="utf-8" /> <meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>BattSim</title> <title>BattSim - Energie Simulator</title>
<base href="/" /> <base href="/" />
<link rel="preload" id="webassembly" /> <link rel="preload" id="webassembly" />
<!-- Main CSS with theme -->
<link rel="stylesheet" href="css/site.css" />
<link rel="stylesheet" href="css/app.css" /> <link rel="stylesheet" href="css/app.css" />
<!-- If you add any scoped CSS files, uncomment the following to load them
<link href="BattSim.styles.css" rel="stylesheet" /> --> <!-- Radzen Blazor CSS -->
<link href="_content/Radzen.Blazor/css/default.css" rel="stylesheet">
<!-- Google Fonts fallback if custom fonts not available -->
<link href="https://fonts.googleapis.com/css2?family=Montserrat:wght@400;700&family=Righteous&display=swap" rel="stylesheet">
<link href="manifest.webmanifest" rel="manifest" /> <link href="manifest.webmanifest" rel="manifest" />
<link rel="apple-touch-icon" sizes="512x512" href="icon-512.png" /> <link rel="apple-touch-icon" sizes="512x512" href="icon-512.png" />
<link rel="apple-touch-icon" sizes="192x192" href="icon-192.png" /> <link rel="apple-touch-icon" sizes="192x192" href="icon-192.png" />
<link rel="stylesheet" href="_content/Radzen.Blazor/css/default.css">
<script type="importmap"></script> <script type="importmap"></script>
</head> </head>
@@ -31,6 +38,7 @@
<a href="." class="reload">Reload</a> <a href="." class="reload">Reload</a>
<span class="dismiss">🗙</span> <span class="dismiss">🗙</span>
</div> </div>
<script src="_framework/blazor.webassembly#[.{fingerprint}].js"></script> <script src="_framework/blazor.webassembly#[.{fingerprint}].js"></script>
<script>navigator.serviceWorker.register('service-worker.js', { updateViaCache: 'none' });</script> <script>navigator.serviceWorker.register('service-worker.js', { updateViaCache: 'none' });</script>
<script src="_content/Radzen.Blazor/Radzen.Blazor.js"></script> <script src="_content/Radzen.Blazor/Radzen.Blazor.js"></script>