Compare commits
8 Commits
3a0647aff2
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
135a869bc5 | ||
| ea8c88f133 | |||
| 295e60502c | |||
| 6efd026394 | |||
|
|
0d7bc18293 | ||
|
|
ac7c5e6013 | ||
|
|
b7726e1974 | ||
| 47d835d336 |
BIN
BattSim Logo.png
Normal file
BIN
BattSim Logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 84 KiB |
34
Dockerfile
Normal file
34
Dockerfile
Normal file
@@ -0,0 +1,34 @@
|
||||
# Build stage
|
||||
FROM mcr.microsoft.com/dotnet/sdk:10.0 AS launch
|
||||
WORKDIR /src
|
||||
|
||||
# Install Python (required for AOT compilation)
|
||||
RUN apt-get update && \
|
||||
apt-get install -y python3 && \
|
||||
ln -s /usr/bin/python3 /usr/bin/python
|
||||
|
||||
|
||||
# Update workloads and SDK
|
||||
RUN dotnet --list-sdks
|
||||
RUN dotnet --list-runtimes
|
||||
RUN dotnet workload update
|
||||
|
||||
# Copy project and solution files
|
||||
COPY *.csproj *.sln ./
|
||||
|
||||
# Install required workloads
|
||||
RUN dotnet workload install wasm-tools
|
||||
|
||||
# Copy the rest of the application
|
||||
COPY . .
|
||||
|
||||
# Restore dependencies
|
||||
RUN dotnet restore
|
||||
|
||||
# Publish the app (optional, if you want to test publish output)
|
||||
# RUN dotnet publish -c Release -o /app/publish
|
||||
|
||||
# Runtime stage: Use the SDK image to run the app
|
||||
ENTRYPOINT ["dotnet", "run", "--no-launch-profile", "--urls", "http://0.0.0.0:5000"]
|
||||
EXPOSE 5000
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
@inherits LayoutComponentBase
|
||||
@inherits LayoutComponentBase
|
||||
|
||||
<main>
|
||||
@Body
|
||||
</main>
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
using System;
|
||||
|
||||
namespace BattSim.Models
|
||||
{
|
||||
public class BatteryDayResult
|
||||
{
|
||||
public DateOnly Date { get; set; }
|
||||
public double ChargedEnergy { get; set; }
|
||||
public double UsedEnergy { get; set; }
|
||||
public double RemainingEnergy { get; set; }
|
||||
public double ReducedConsumption { get; set; }
|
||||
public double ReducedProduction { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -8,5 +8,16 @@ namespace BattSim.Models
|
||||
public bool DayTariff { get; set; }
|
||||
public double Consumption { get; set; }
|
||||
public double Production { get; set; }
|
||||
public double BatteryCharge { get; set; }
|
||||
|
||||
public EnergyData(){}
|
||||
public EnergyData(EnergyData other)
|
||||
{
|
||||
Time = other.Time;
|
||||
DayTariff = other.DayTariff;
|
||||
Consumption = other.Consumption;
|
||||
Production = other.Production;
|
||||
BatteryCharge = other.BatteryCharge;
|
||||
}
|
||||
}
|
||||
}
|
||||
530
Pages/Home.razor
530
Pages/Home.razor
@@ -1,121 +1,499 @@
|
||||
@page "/"
|
||||
@page "/"
|
||||
@using Radzen
|
||||
@using Radzen.Blazor
|
||||
@using BattSim.Models
|
||||
@using BattSim.Services
|
||||
|
||||
<PageTitle>BattSim</PageTitle>
|
||||
<PageTitle>Thuisbatterij Simulator</PageTitle>
|
||||
|
||||
<h1>BattSim</h1>
|
||||
<h1 style="color: white">Thuisbatterij Simulator</h1>
|
||||
|
||||
<h2>Input Data</h2>
|
||||
<div>
|
||||
<p>Upload your Fluvius quarterly csv file here. The longer the timeframe the longer it takes to process.</p>
|
||||
<InputFile OnChange="OnFileUploaded" accept=".csv"/>
|
||||
@if (_isLoadingFile)
|
||||
<div class="pipeline-container">
|
||||
<!-- Pipeline with 3 connected boxes -->
|
||||
<div class="pipeline-row">
|
||||
<div class="pipeline-connector"></div>
|
||||
|
||||
<!-- Box 1: Data Upload -->
|
||||
<div class="pipeline-box @GetPipelineStepClasses(0)">
|
||||
<div class="box-header" @onclick="() => ToggleBox(0)">
|
||||
<div class="step-number">1</div>
|
||||
<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)
|
||||
{
|
||||
<p>Loading...</p>
|
||||
<div class="loading-indicator">
|
||||
<div class="spinner"></div>
|
||||
<span style="margin-left: 10px;">Data aan het inladen...</span>
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
@if (FluviusDataRaw.Count != 0)
|
||||
@if (FluviusDataRaw.Length == 0)
|
||||
{
|
||||
<p>@(FluviusDataRaw.Count) entries read.</p>
|
||||
<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>
|
||||
}
|
||||
|
||||
@if (FluviusDataDaily.Count != 0)
|
||||
<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)
|
||||
{
|
||||
<RadzenChart>
|
||||
<RadzenAreaSeries Smooth=true Data="@FluviusDataDaily.Values" CategoryProperty="Time" Title="Consumption" ValueProperty="Consumption">
|
||||
<RadzenChartTooltipOptions Visible="true"/>
|
||||
</RadzenAreaSeries>
|
||||
<RadzenAreaSeries Smooth=true Data="@FluviusDataDaily.Values" 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 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>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
<!-- Chart - Directly on page, full width -->
|
||||
@if (FilteredFluviusData.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>
|
||||
Volledige periode
|
||||
</button>
|
||||
<button class="@GetTimeButtonClass("year")" @onclick="SetTimePeriodYear" @onclick:stopPropagation>
|
||||
Laatste jaar
|
||||
</button>
|
||||
<button class="@GetTimeButtonClass("month")" @onclick="SetTimePeriodMonth" @onclick:stopPropagation>
|
||||
Laatste maand
|
||||
</button>
|
||||
<button class="@GetTimeButtonClass("week")" @onclick="SetTimePeriodWeek" @onclick:stopPropagation>
|
||||
Laatste week
|
||||
</button>
|
||||
<button class="@GetTimeButtonClass("day")" @onclick="SetTimePeriodDay" @onclick:stopPropagation>
|
||||
Laatste 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 (FilteredSimulationData.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 {
|
||||
Dictionary<DateTime, EnergyData> FluviusDataRaw = [];
|
||||
Dictionary<DateOnly, EnergyData> FluviusDataDaily = [];
|
||||
bool _isLoadingFile = false;
|
||||
EnergyData[] FluviusDataRaw = [];
|
||||
EnergyData[] SimulationData = [];
|
||||
|
||||
DataFilter.FilterOption SelectedFilterOption = DataFilter.FilterOption.ALL;
|
||||
|
||||
EnergyData[] FilteredFluviusData = [];
|
||||
EnergyData[] 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)
|
||||
{
|
||||
_uploadError = null;
|
||||
var file = e.File;
|
||||
_uploadedFileName = file.Name;
|
||||
|
||||
if (file.ContentType != "text/csv")
|
||||
{
|
||||
Console.WriteLine("Only CSV files are allowed!");
|
||||
_uploadError = "Alleen CSV bestanden zijn toegestaan!";
|
||||
StateHasChanged();
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
Console.WriteLine("Reading csv file...");
|
||||
_isLoadingFile = true;
|
||||
StepData[0].isProcessing = true;
|
||||
StateHasChanged();
|
||||
|
||||
FluviusDataRaw = await FluviusDataHandler.LoadAndProcessFile(file);
|
||||
FluviusDataDaily = FluviusDataHandler.GenerateDailyData(FluviusDataRaw);
|
||||
_isLoadingFile = false;
|
||||
SetTimePeriod("all");
|
||||
FilteredSimulationData = [];
|
||||
|
||||
StepData[0].isProcessing = false;
|
||||
StepData[0].Completed = true;
|
||||
StepData[1].Completed = false;
|
||||
StepData[2].Completed = false;
|
||||
SimulationData = [];
|
||||
|
||||
StateHasChanged();
|
||||
Console.WriteLine("Done reading csv file!");
|
||||
}
|
||||
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);
|
||||
SetTimePeriod("all");
|
||||
|
||||
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;
|
||||
var filterOption = period switch {
|
||||
"all" => DataFilter.FilterOption.ALL,
|
||||
"year" => DataFilter.FilterOption.YEAR,
|
||||
"month" => DataFilter.FilterOption.MONTH,
|
||||
"week" => DataFilter.FilterOption.WEEK,
|
||||
"day" => DataFilter.FilterOption.DAY,
|
||||
};
|
||||
// Filter data based on selected period
|
||||
if(FluviusDataRaw.Length > 0) FilteredFluviusData = DataFilter.FilterData(FluviusDataRaw, filterOption);
|
||||
if(SimulationData.Length > 0) FilteredSimulationData = DataFilter.FilterData(SimulationData, filterOption);
|
||||
|
||||
StateHasChanged();
|
||||
}
|
||||
|
||||
private string FormatObject(object value) {
|
||||
if(value is double d) return $"{d:0.##} kWh";
|
||||
if(value is DateTime date) return date.ToString("dd/MM/yyyy");
|
||||
if(value is DateTime time) return time.ToString("dd/MM/yyyy");
|
||||
if(value is DateOnly date) return date.ToString("dd/MM/yyyy");
|
||||
return string.Empty;
|
||||
}
|
||||
}
|
||||
|
||||
<h2>Simulate Battery</h2>
|
||||
<div>
|
||||
<p>Generate adjusted energy data simulating the effect of a battery with properties as configured here:</p>
|
||||
<ul>
|
||||
<li>
|
||||
<p>Capacity: <InputNumber @bind-value="BatteryCapacity"/> kWh</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>Discharge Rate: <InputNumber @bind-value="DischargeRate"/> kW</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>Charge Rate: <InputNumber @bind-value="ChargeRate"/> kW</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>Round-trip Efficiency: <InputNumber @bind-value="Efficiency"/> %</p>
|
||||
</li>
|
||||
</ul>
|
||||
<Button @onclick="SimulateBattery">Simulate</Button>
|
||||
</div>
|
||||
|
||||
@code {
|
||||
double BatteryCapacity = 7.5;
|
||||
double DischargeRate = 3;
|
||||
double ChargeRate = 3;
|
||||
double Efficiency = 90;
|
||||
BatteryDayResult[] SimulationData = [];
|
||||
|
||||
private async Task SimulateBattery(){
|
||||
Console.WriteLine("Simulating...");
|
||||
// SimulationData = BatterySimulator.SimulateBattery(EnergyData, BatteryCapacity).ToArray();
|
||||
Console.WriteLine("Done simulating!");
|
||||
}
|
||||
}
|
||||
|
||||
<h2>Calculate Cost</h2>
|
||||
|
||||
@@ -4,7 +4,6 @@ using Radzen;
|
||||
using Microsoft.AspNetCore.Components.Web;
|
||||
using Microsoft.AspNetCore.Components.WebAssembly.Hosting;
|
||||
|
||||
// Setup Frontend
|
||||
var builder = WebAssemblyHostBuilder.CreateDefault(args);
|
||||
builder.RootComponents.Add<App>("#app");
|
||||
builder.RootComponents.Add<HeadOutlet>("head::after");
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using BattSim.Models;
|
||||
|
||||
@@ -5,39 +6,32 @@ namespace BattSim.Services
|
||||
{
|
||||
public static class BatterySimulator
|
||||
{
|
||||
public static List<BatteryDayResult> SimulateBattery(EnergyData[] data, double batteryCapacity)
|
||||
public static EnergyData[] SimulateBattery(EnergyData[] energyData, double batteryCapacity, double efficiency)
|
||||
{
|
||||
var results = new List<BatteryDayResult>();
|
||||
double remainingEnergy = 0;
|
||||
var results = new List<EnergyData>();
|
||||
double batteryCharge = 0;
|
||||
|
||||
foreach (var day in data)
|
||||
foreach (var e in energyData)
|
||||
{
|
||||
// // Charge battery from production
|
||||
// var chargedEnergy = System.Math.Min(day.TotalProduction, batteryCapacity);
|
||||
// var excessProduction = day.TotalProduction - chargedEnergy;
|
||||
var simulatedBatteryEnergyData = new EnergyData(e);
|
||||
// Simulate charging the battery based on production
|
||||
double excessProduction = batteryCharge + e.Production - batteryCapacity;
|
||||
batteryCharge = double.Min(batteryCharge + e.Production, batteryCapacity);
|
||||
simulatedBatteryEnergyData.Production = double.Max(excessProduction, 0);
|
||||
|
||||
// // Use battery for consumption
|
||||
// var usedEnergy = System.Math.Min(chargedEnergy + remainingEnergy, day.TotalConsumption);
|
||||
// var remainingAfterUse = chargedEnergy + remainingEnergy - usedEnergy;
|
||||
// Simulate discharging the battery based on consumption
|
||||
double availableEnergy = batteryCharge * efficiency;
|
||||
double deficit = e.Consumption - availableEnergy;
|
||||
double energyDrawn = Math.Min(e.Consumption, availableEnergy);
|
||||
batteryCharge = batteryCharge - (energyDrawn / efficiency); // Adjust for loss
|
||||
simulatedBatteryEnergyData.Consumption = Math.Max(deficit, 0);
|
||||
|
||||
// // Calculate reduced values
|
||||
// var reducedConsumption = System.Math.Min(usedEnergy, day.TotalConsumption);
|
||||
// var reducedProduction = day.TotalProduction - chargedEnergy;
|
||||
|
||||
// results.Add(new BatteryDayResult
|
||||
// {
|
||||
// Date = day.Date,
|
||||
// ChargedEnergy = chargedEnergy,
|
||||
// UsedEnergy = usedEnergy,
|
||||
// RemainingEnergy = remainingAfterUse,
|
||||
// ReducedConsumption = reducedConsumption,
|
||||
// ReducedProduction = reducedProduction
|
||||
// });
|
||||
|
||||
// remainingEnergy = remainingAfterUse;
|
||||
// Register the current charge
|
||||
simulatedBatteryEnergyData.BatteryCharge = batteryCharge;
|
||||
results.Add(simulatedBatteryEnergyData);
|
||||
}
|
||||
|
||||
return results;
|
||||
return results.ToArray();
|
||||
}
|
||||
}
|
||||
}
|
||||
30
Services/DataFilter.cs
Normal file
30
Services/DataFilter.cs
Normal file
@@ -0,0 +1,30 @@
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using BattSim.Models;
|
||||
|
||||
namespace BattSim.Services
|
||||
{
|
||||
public static class DataFilter
|
||||
{
|
||||
public enum FilterOption { ALL, YEAR, MONTH, WEEK, DAY }
|
||||
public static EnergyData[] FilterData(EnergyData[] data, FilterOption filterOption)
|
||||
{
|
||||
var dailyData = data
|
||||
.GroupBy(d=>d.Time.Date)
|
||||
.Select(group=> new EnergyData{
|
||||
Consumption = group.Sum(d => d.Consumption),
|
||||
Production = group.Sum(d => d.Production),
|
||||
BatteryCharge = group.Max(d => d.BatteryCharge),
|
||||
Time = group.First().Time
|
||||
}).ToArray();
|
||||
|
||||
return filterOption switch {
|
||||
FilterOption.DAY => data[(data.Length > 96?data.Length - 96:0)..], // 24 hours * 4 quarters per hour
|
||||
FilterOption.WEEK => data[(data.Length > 672?data.Length - 672:0)..], // 7 days * 24 hours * 4 quarters per hour
|
||||
FilterOption.MONTH => dailyData[(dailyData.Length > 30?dailyData.Length - 30:0)..],
|
||||
FilterOption.YEAR => dailyData[(dailyData.Length > 365?dailyData.Length - 365:0)..],
|
||||
_ => dailyData
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
87
Services/EnergyCostCalculator.cs
Normal file
87
Services/EnergyCostCalculator.cs
Normal file
@@ -0,0 +1,87 @@
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Reflection.PortableExecutable;
|
||||
using System.Threading.Tasks;
|
||||
using BattSim.Models;
|
||||
using Microsoft.AspNetCore.Components.Forms;
|
||||
|
||||
namespace BattSim.Services
|
||||
{
|
||||
public class EnergyCostCalculator
|
||||
{
|
||||
public double BasePayment { get; set; } = 35;
|
||||
public double EnergyCost { get; set; } = 0.1364;
|
||||
public double ReturnCost { get; set; } = 0.0266;
|
||||
public double GreenCertificateCost { get; set; } = 0.01172;
|
||||
public double HeatingCertificateCost { get; set; } = 0.00415;
|
||||
public double DataManagementCost { get; set; } = 18.77;
|
||||
public double CapacityCost { get; set; } = 58.20;
|
||||
public double UsageTariff { get; set; } = 0.0637;
|
||||
public double EnergyContribution { get; set; } = 0.00205;
|
||||
public double SpecialTariffs { get; set; } = 0.0502;
|
||||
public double FlemishEnergyTariff { get; set; } = 118.56;
|
||||
|
||||
public double CalculateCostOfEnergyUsage(EnergyData[] energyData, bool print = false) {
|
||||
int amountOfDays = energyData.Last().Time.Subtract(energyData.First().Time).Days;
|
||||
double yearfactor = amountOfDays / 365.0;
|
||||
double energyConsumption = energyData.Sum(e => e.Consumption);
|
||||
double energyProduction = energyData.Sum(e => e.Production);
|
||||
|
||||
double baseCost = yearfactor * BasePayment;
|
||||
if(print) Console.WriteLine($"Base Cost: €{baseCost}");
|
||||
|
||||
double energyCost = energyConsumption * EnergyCost;
|
||||
if (print) Console.WriteLine($"Energy Cost: €{energyCost}");
|
||||
|
||||
double returnCost = energyProduction * ReturnCost;
|
||||
if (print) Console.WriteLine($"Return Cost: € -{returnCost}");
|
||||
|
||||
double greenCertificateCost = energyConsumption * GreenCertificateCost;
|
||||
if (print) Console.WriteLine($"Green Certificate Cost: €{greenCertificateCost}");
|
||||
|
||||
double heatingCertificateCost = energyConsumption * HeatingCertificateCost;
|
||||
if (print) Console.WriteLine($"Heating Certificate Cost: €{heatingCertificateCost}");
|
||||
|
||||
double dataManagmentCost = yearfactor * DataManagementCost;
|
||||
if (print) Console.WriteLine($"Data Management Cost: €{dataManagmentCost}");
|
||||
|
||||
double capacityCost = CalculateYearCapacity(energyData) * yearfactor;
|
||||
if (print) Console.WriteLine($"Capacity Cost: €{capacityCost}");
|
||||
|
||||
double usageTariffCost = energyConsumption * UsageTariff;
|
||||
if (print) Console.WriteLine($"Usage Tariff Cost: €{usageTariffCost}");
|
||||
|
||||
double energyContributionCost = energyConsumption * EnergyContribution;
|
||||
if (print) Console.WriteLine($"Energy Contribution Cost: €{energyContributionCost}");
|
||||
|
||||
double specialTariffsCost = energyConsumption * SpecialTariffs;
|
||||
if (print) Console.WriteLine($"Special Tariffs Cost: €{specialTariffsCost}");
|
||||
|
||||
double flemishEnergyTarif = yearfactor * FlemishEnergyTariff;
|
||||
if (print) Console.WriteLine($"Flemish Tariff: €{specialTariffsCost}");
|
||||
|
||||
return baseCost + energyCost - returnCost + greenCertificateCost + heatingCertificateCost + dataManagmentCost +
|
||||
capacityCost + usageTariffCost + energyContributionCost + specialTariffsCost + flemishEnergyTarif;
|
||||
}
|
||||
|
||||
|
||||
private double CalculateYearCapacity(EnergyData[] energyData)
|
||||
{
|
||||
double peakSum = 0;
|
||||
int months = 0;
|
||||
for(int i = 0; i<12; i++)
|
||||
{
|
||||
var monthData = energyData.Where(e => e.Time.Month == i);
|
||||
if (monthData.Count() == 0) continue;
|
||||
peakSum += monthData.Max(e => e.Consumption) * 4;
|
||||
months++;
|
||||
}
|
||||
return (peakSum / months) * CapacityCost;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -13,7 +13,7 @@ namespace BattSim.Services
|
||||
{
|
||||
public static class FluviusDataHandler
|
||||
{
|
||||
public static async Task<Dictionary<DateTime, EnergyData>> LoadAndProcessFile(IBrowserFile file)
|
||||
public static async Task<EnergyData[]> LoadAndProcessFile(IBrowserFile file)
|
||||
{
|
||||
var lines = await ReadCsvFile(file);
|
||||
var energyData = new ConcurrentDictionary<DateTime,EnergyData>(); // Thread-safe collection
|
||||
@@ -48,21 +48,22 @@ namespace BattSim.Services
|
||||
Console.WriteLine($"Error parsing line: {line}. Skipping to next line. Exception: {e}");
|
||||
}
|
||||
});
|
||||
return energyData.ToDictionary();
|
||||
var results = energyData.Values.ToArray();
|
||||
results.Sort((a,b)=>a.Time.CompareTo(b.Time));
|
||||
return results;
|
||||
}
|
||||
|
||||
public static Dictionary<DateOnly, EnergyData> GenerateDailyData(Dictionary<DateTime, EnergyData> energyData)
|
||||
public static EnergyData[] GenerateDailyData(EnergyData[] energyData)
|
||||
{
|
||||
var dailyEnergyData = new ConcurrentDictionary<DateOnly, EnergyData>();
|
||||
Parallel.ForEach(energyData, entry =>
|
||||
Parallel.ForEach(energyData, energy =>
|
||||
{
|
||||
var date = DateOnly.FromDateTime(entry.Key);
|
||||
var energy = entry.Value;
|
||||
var date = DateOnly.FromDateTime(energy.Time);
|
||||
|
||||
// Use AddOrUpdate to avoid double lookup
|
||||
dailyEnergyData.AddOrUpdate(
|
||||
date,
|
||||
energy, // If key doesn't exist, add this value
|
||||
new EnergyData(energy), // If key doesn't exist, add this value
|
||||
(_, existing) =>
|
||||
{
|
||||
// If key exists, aggregate the values
|
||||
@@ -72,7 +73,7 @@ namespace BattSim.Services
|
||||
}
|
||||
);
|
||||
});
|
||||
return dailyEnergyData.ToDictionary();
|
||||
return dailyEnergyData.Values.ToArray();
|
||||
}
|
||||
|
||||
private static async Task<List<string>> ReadCsvFile(IBrowserFile file)
|
||||
@@ -84,7 +85,7 @@ namespace BattSim.Services
|
||||
await reader.ReadLineAsync();
|
||||
// Read all lines into memory
|
||||
var lines = new List<string>();
|
||||
string line;
|
||||
string? line;
|
||||
while ((line = await reader.ReadLineAsync()) is not null)
|
||||
{
|
||||
lines.Add(line);
|
||||
|
||||
19585
data/Verbruikshistoriek_afrekening_jaar_1.csv
Normal file
19585
data/Verbruikshistoriek_afrekening_jaar_1.csv
Normal file
File diff suppressed because it is too large
Load Diff
5761
data/Verbruikshistoriek_april_26.csv
Normal file
5761
data/Verbruikshistoriek_april_26.csv
Normal file
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
10
docker-compose.yml
Normal file
10
docker-compose.yml
Normal file
@@ -0,0 +1,10 @@
|
||||
version: '3.8'
|
||||
|
||||
services:
|
||||
web:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile
|
||||
ports:
|
||||
- "5000:5000"
|
||||
restart: unless-stopped
|
||||
708
wwwroot/css/site.css
Normal file
708
wwwroot/css/site.css
Normal 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);
|
||||
}
|
||||
BIN
wwwroot/fonts/Montserrat.ttf
Normal file
BIN
wwwroot/fonts/Montserrat.ttf
Normal file
Binary file not shown.
BIN
wwwroot/fonts/Righteous.ttf
Normal file
BIN
wwwroot/fonts/Righteous.ttf
Normal file
Binary file not shown.
@@ -4,16 +4,23 @@
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>BattSim</title>
|
||||
<title>BattSim - Energie Simulator</title>
|
||||
<base href="/" />
|
||||
<link rel="preload" id="webassembly" />
|
||||
|
||||
<!-- Main CSS with theme -->
|
||||
<link rel="stylesheet" href="css/site.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 rel="apple-touch-icon" sizes="512x512" href="icon-512.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>
|
||||
</head>
|
||||
|
||||
@@ -31,6 +38,7 @@
|
||||
<a href="." class="reload">Reload</a>
|
||||
<span class="dismiss">🗙</span>
|
||||
</div>
|
||||
|
||||
<script src="_framework/blazor.webassembly#[.{fingerprint}].js"></script>
|
||||
<script>navigator.serviceWorker.register('service-worker.js', { updateViaCache: 'none' });</script>
|
||||
<script src="_content/Radzen.Blazor/Radzen.Blazor.js"></script>
|
||||
|
||||
Reference in New Issue
Block a user