From 6cf14e34837e026db13f4c0d4e3f0395801a153c Mon Sep 17 00:00:00 2001 From: Yinyin Liu Date: Tue, 10 Mar 2026 12:32:01 +0100 Subject: [PATCH] daily tab design with hourly data and last week quick entry with self-efficiency on top --- csharp/App/Backend/Controller.cs | 113 ++++ csharp/App/Backend/Database/Read.cs | 20 +- .../Backend/Services/WeeklyReportService.cs | 2 +- .../SodiohomeInstallations/DailySection.tsx | 614 ++++++++++++++++++ .../SodiohomeInstallations/WeeklyReport.tsx | 160 +---- typescript/frontend-marios2/src/lang/de.json | 7 + typescript/frontend-marios2/src/lang/en.json | 7 + typescript/frontend-marios2/src/lang/fr.json | 7 + typescript/frontend-marios2/src/lang/it.json | 7 + 9 files changed, 764 insertions(+), 173 deletions(-) create mode 100644 typescript/frontend-marios2/src/content/dashboards/SodiohomeInstallations/DailySection.tsx diff --git a/csharp/App/Backend/Controller.cs b/csharp/App/Backend/Controller.cs index 28430c240..867a15a9e 100644 --- a/csharp/App/Backend/Controller.cs +++ b/csharp/App/Backend/Controller.cs @@ -1235,6 +1235,119 @@ public class Controller : ControllerBase return Ok(new { count = records.Count, records }); } + [HttpGet(nameof(GetHourlyRecords))] + public ActionResult> GetHourlyRecords( + Int64 installationId, String from, String to, Token authToken) + { + var user = Db.GetSession(authToken)?.User; + if (user == null) + return Unauthorized(); + + var installation = Db.GetInstallationById(installationId); + if (installation is null || !user.HasAccessTo(installation)) + return Unauthorized(); + + if (!DateOnly.TryParseExact(from, "yyyy-MM-dd", out var fromDate) || + !DateOnly.TryParseExact(to, "yyyy-MM-dd", out var toDate)) + return BadRequest("from and to must be in yyyy-MM-dd format."); + + var records = Db.GetHourlyRecords(installationId, fromDate, toDate); + return Ok(new { count = records.Count, records }); + } + + /// + /// Returns daily + hourly records for a date range. + /// DB first; if empty, falls back to xlsx parsing and caches results for future calls. + /// + [HttpGet(nameof(GetDailyDetailRecords))] + public ActionResult GetDailyDetailRecords( + Int64 installationId, String from, String to, Token authToken) + { + var user = Db.GetSession(authToken)?.User; + if (user == null) + return Unauthorized(); + + var installation = Db.GetInstallationById(installationId); + if (installation is null || !user.HasAccessTo(installation)) + return Unauthorized(); + + if (!DateOnly.TryParseExact(from, "yyyy-MM-dd", out var fromDate) || + !DateOnly.TryParseExact(to, "yyyy-MM-dd", out var toDate)) + return BadRequest("from and to must be in yyyy-MM-dd format."); + + // 1. Try DB + var dailyRecords = Db.GetDailyRecords(installationId, fromDate, toDate); + var hourlyRecords = Db.GetHourlyRecords(installationId, fromDate, toDate); + + // 2. Fallback: parse xlsx + cache to DB + if (dailyRecords.Count == 0 || hourlyRecords.Count == 0) + { + var xlsxFiles = WeeklyReportService.GetRelevantXlsxFiles(installationId, fromDate, toDate); + if (xlsxFiles.Count > 0) + { + foreach (var xlsxPath in xlsxFiles) + { + if (dailyRecords.Count == 0) + { + foreach (var day in ExcelDataParser.Parse(xlsxPath)) + { + if (Db.DailyRecordExists(installationId, day.Date)) + continue; + Db.Create(new DailyEnergyRecord + { + InstallationId = installationId, + Date = day.Date, + PvProduction = day.PvProduction, + LoadConsumption = day.LoadConsumption, + GridImport = day.GridImport, + GridExport = day.GridExport, + BatteryCharged = day.BatteryCharged, + BatteryDischarged = day.BatteryDischarged, + CreatedAt = DateTime.UtcNow.ToString("o"), + }); + } + } + + if (hourlyRecords.Count == 0) + { + foreach (var hour in ExcelDataParser.ParseHourly(xlsxPath)) + { + var dateHour = $"{hour.DateTime:yyyy-MM-dd HH}"; + if (Db.HourlyRecordExists(installationId, dateHour)) + continue; + Db.Create(new HourlyEnergyRecord + { + InstallationId = installationId, + Date = hour.DateTime.ToString("yyyy-MM-dd"), + Hour = hour.Hour, + DateHour = dateHour, + DayOfWeek = hour.DayOfWeek, + IsWeekend = hour.IsWeekend, + PvKwh = hour.PvKwh, + LoadKwh = hour.LoadKwh, + GridImportKwh = hour.GridImportKwh, + BatteryChargedKwh = hour.BatteryChargedKwh, + BatteryDischargedKwh = hour.BatteryDischargedKwh, + BattSoC = hour.BattSoC, + CreatedAt = DateTime.UtcNow.ToString("o"), + }); + } + } + } + + // Re-read from DB (now cached) + dailyRecords = Db.GetDailyRecords(installationId, fromDate, toDate); + hourlyRecords = Db.GetHourlyRecords(installationId, fromDate, toDate); + } + } + + return Ok(new + { + dailyRecords = new { count = dailyRecords.Count, records = dailyRecords }, + hourlyRecords = new { count = hourlyRecords.Count, records = hourlyRecords }, + }); + } + /// /// Deletes DailyEnergyRecord rows for an installation in the given date range. /// Safe to use during testing — only removes daily records, not report summaries. diff --git a/csharp/App/Backend/Database/Read.cs b/csharp/App/Backend/Database/Read.cs index 49fb192dc..b817a3f26 100644 --- a/csharp/App/Backend/Database/Read.cs +++ b/csharp/App/Backend/Database/Read.cs @@ -116,13 +116,9 @@ public static partial class Db { var fromStr = from.ToString("yyyy-MM-dd"); var toStr = to.ToString("yyyy-MM-dd"); - return DailyRecords - .Where(r => r.InstallationId == installationId) - .ToList() - .Where(r => String.Compare(r.Date, fromStr, StringComparison.Ordinal) >= 0 - && String.Compare(r.Date, toStr, StringComparison.Ordinal) <= 0) - .OrderBy(r => r.Date) - .ToList(); + return Connection.Query( + "SELECT * FROM DailyEnergyRecord WHERE InstallationId = ? AND Date >= ? AND Date <= ? ORDER BY Date", + installationId, fromStr, toStr); } /// @@ -141,13 +137,9 @@ public static partial class Db { var fromStr = from.ToString("yyyy-MM-dd"); var toStr = to.ToString("yyyy-MM-dd"); - return HourlyRecords - .Where(r => r.InstallationId == installationId) - .ToList() - .Where(r => String.Compare(r.Date, fromStr, StringComparison.Ordinal) >= 0 - && String.Compare(r.Date, toStr, StringComparison.Ordinal) <= 0) - .OrderBy(r => r.Date).ThenBy(r => r.Hour) - .ToList(); + return Connection.Query( + "SELECT * FROM HourlyEnergyRecord WHERE InstallationId = ? AND Date >= ? AND Date <= ? ORDER BY Date, Hour", + installationId, fromStr, toStr); } /// diff --git a/csharp/App/Backend/Services/WeeklyReportService.cs b/csharp/App/Backend/Services/WeeklyReportService.cs index b2fcde817..4c841f470 100644 --- a/csharp/App/Backend/Services/WeeklyReportService.cs +++ b/csharp/App/Backend/Services/WeeklyReportService.cs @@ -15,7 +15,7 @@ public static class WeeklyReportService /// Filename pattern: {installationId}_MMDD_MMDD.xlsx (e.g. "848_0302_0308.xlsx") /// Falls back to all files if filenames can't be parsed. /// - private static List GetRelevantXlsxFiles(long installationId, DateOnly rangeStart, DateOnly rangeEnd) + public static List GetRelevantXlsxFiles(long installationId, DateOnly rangeStart, DateOnly rangeEnd) { if (!Directory.Exists(TmpReportDir)) return new List(); diff --git a/typescript/frontend-marios2/src/content/dashboards/SodiohomeInstallations/DailySection.tsx b/typescript/frontend-marios2/src/content/dashboards/SodiohomeInstallations/DailySection.tsx new file mode 100644 index 000000000..75236921c --- /dev/null +++ b/typescript/frontend-marios2/src/content/dashboards/SodiohomeInstallations/DailySection.tsx @@ -0,0 +1,614 @@ +import { useEffect, useState, useMemo } from 'react'; +import { useIntl, FormattedMessage } from 'react-intl'; +import { + Alert, + Box, + CircularProgress, + Container, + Grid, + Paper, + TextField, + Typography +} from '@mui/material'; +import { Line } from 'react-chartjs-2'; +import { + Chart, + CategoryScale, + LinearScale, + PointElement, + LineElement, + Filler, + Tooltip, + Legend +} from 'chart.js'; +import axiosConfig from 'src/Resources/axiosConfig'; +import { SavingsCards } from './WeeklyReport'; + +Chart.register( + CategoryScale, + LinearScale, + PointElement, + LineElement, + Filler, + Tooltip, + Legend +); + +// ── Interfaces ─────────────────────────────────────────────── + +interface DailyEnergyData { + date: string; + pvProduction: number; + loadConsumption: number; + gridImport: number; + gridExport: number; + batteryCharged: number; + batteryDischarged: number; +} + +interface HourlyEnergyRecord { + date: string; + hour: number; + pvKwh: number; + loadKwh: number; + gridImportKwh: number; + batteryChargedKwh: number; + batteryDischargedKwh: number; + battSoC: number; +} + +// ── Date Helpers ───────────────────────────────────────────── + +/** + * Anchor date for the 7-day strip. Returns last completed Sunday. + * To switch to live-data mode later, change to: () => new Date() + */ +function getDataAnchorDate(): Date { + const today = new Date(); + const dow = today.getDay(); // 0=Sun + const lastSunday = new Date(today); + lastSunday.setDate(today.getDate() - (dow === 0 ? 7 : dow)); + lastSunday.setHours(0, 0, 0, 0); + return lastSunday; +} + +function getWeekRange(anchor: Date): { monday: Date; sunday: Date } { + const sunday = new Date(anchor); + const monday = new Date(sunday); + monday.setDate(sunday.getDate() - 6); + return { monday, sunday }; +} + +function formatDateISO(d: Date): string { + const y = d.getFullYear(); + const m = String(d.getMonth() + 1).padStart(2, '0'); + const day = String(d.getDate()).padStart(2, '0'); + return `${y}-${m}-${day}`; +} + +function getWeekDays(monday: Date): Date[] { + return Array.from({ length: 7 }, (_, i) => { + const d = new Date(monday); + d.setDate(monday.getDate() + i); + return d; + }); +} + +// ── Main Component ─────────────────────────────────────────── + +export default function DailySection({ + installationId +}: { + installationId: number; +}) { + const intl = useIntl(); + const anchor = useMemo(() => getDataAnchorDate(), []); + const { monday, sunday } = useMemo(() => getWeekRange(anchor), [anchor]); + const weekDays = useMemo(() => getWeekDays(monday), [monday]); + + const [weekRecords, setWeekRecords] = useState([]); + const [weekHourlyRecords, setWeekHourlyRecords] = useState([]); + const [selectedDate, setSelectedDate] = useState(formatDateISO(sunday)); + const [selectedDayRecord, setSelectedDayRecord] = useState(null); + const [hourlyRecords, setHourlyRecords] = useState([]); + const [loadingWeek, setLoadingWeek] = useState(false); + const [noData, setNoData] = useState(false); + + // Fetch week data (daily + hourly) via combined endpoint with xlsx fallback + cache + useEffect(() => { + setLoadingWeek(true); + axiosConfig + .get('/GetDailyDetailRecords', { + params: { + installationId, + from: formatDateISO(monday), + to: formatDateISO(sunday) + } + }) + .then((res) => { + const daily = res.data?.dailyRecords?.records ?? []; + const hourly = res.data?.hourlyRecords?.records ?? []; + setWeekRecords(Array.isArray(daily) ? daily : []); + setWeekHourlyRecords(Array.isArray(hourly) ? hourly : []); + }) + .catch(() => { + setWeekRecords([]); + setWeekHourlyRecords([]); + }) + .finally(() => setLoadingWeek(false)); + }, [installationId, monday, sunday]); + + // When selected date changes, extract data from week cache or fetch + useEffect(() => { + setNoData(false); + setSelectedDayRecord(null); + + // Try week cache first + const cachedDay = weekRecords.find((r) => r.date === selectedDate); + const cachedHours = weekHourlyRecords.filter((r) => r.date === selectedDate); + + if (cachedDay) { + setSelectedDayRecord(cachedDay); + setHourlyRecords(cachedHours); + return; + } + + // Not in cache (date picker outside strip) — fetch via combined endpoint + axiosConfig + .get('/GetDailyDetailRecords', { + params: { installationId, from: selectedDate, to: selectedDate } + }) + .then((res) => { + const daily = res.data?.dailyRecords?.records ?? []; + const hourly = res.data?.hourlyRecords?.records ?? []; + setHourlyRecords(Array.isArray(hourly) ? hourly : []); + if (Array.isArray(daily) && daily.length > 0) { + setSelectedDayRecord(daily[0]); + } else { + setNoData(true); + } + }) + .catch(() => { + setHourlyRecords([]); + setNoData(true); + }); + }, [installationId, selectedDate, weekRecords, weekHourlyRecords]); + + const record = selectedDayRecord; + + const kpis = useMemo(() => computeKPIs(record), [record]); + + const handleDatePicker = (e: React.ChangeEvent) => { + setSelectedDate(e.target.value); + }; + + const handleStripSelect = (date: string) => { + setSelectedDate(date); + setNoData(false); + }; + + const dt = new Date(selectedDate + 'T00:00:00'); + const dateLabel = dt.toLocaleDateString(intl.locale, { + weekday: 'long', + year: 'numeric', + month: 'long', + day: 'numeric' + }); + + return ( + <> + {/* Date Picker */} + + + + + + + + {/* 7-Day Strip */} + + + {/* Loading state */} + {loadingWeek && !record && ( + + + + )} + + {/* No data state */} + {!loadingWeek && noData && !record && ( + + + + )} + + {/* Day detail */} + {record && ( + <> + {/* Header */} + + + + + + {dateLabel} + + + + {/* KPI Cards */} + + + + + {/* Intraday Chart */} + + + {/* Summary Table */} + + + )} + + ); +} + +// ── KPI Computation ────────────────────────────────────────── + +function computeKPIs(record: DailyEnergyData | null) { + if (!record) { + return { energySaved: 0, savingsCHF: 0, selfSufficiency: 0, batteryEfficiency: 0 }; + } + const energySaved = Math.max(0, record.loadConsumption - record.gridImport); + const savingsCHF = +(energySaved * 0.39).toFixed(2); + const selfSufficiency = + record.loadConsumption > 0 + ? Math.min(100, (1 - record.gridImport / record.loadConsumption) * 100) + : 0; + const batteryEfficiency = + record.batteryCharged > 0 + ? Math.min(100, Math.floor((record.batteryDischarged / record.batteryCharged) * 100)) + : 0; + return { energySaved, savingsCHF, selfSufficiency, batteryEfficiency }; +} + +// ── DayStrip ───────────────────────────────────────────────── + +function DayStrip({ + weekDays, + weekRecords, + selectedDate, + onSelect, + sunday, + loading +}: { + weekDays: Date[]; + weekRecords: DailyEnergyData[]; + selectedDate: string; + onSelect: (date: string) => void; + sunday: Date; + loading: boolean; +}) { + const intl = useIntl(); + const sundayLabel = sunday.toLocaleDateString(intl.locale, { + year: 'numeric', + month: 'short', + day: 'numeric' + }); + + return ( + + + {weekDays.map((day) => { + const dateStr = formatDateISO(day); + const isSelected = dateStr === selectedDate; + const record = weekRecords.find((r) => r.date === dateStr); + const selfSuff = + record && record.loadConsumption > 0 + ? Math.min(100, (1 - record.gridImport / record.loadConsumption) * 100) + : null; + + return ( + onSelect(dateStr)} + elevation={isSelected ? 4 : 1} + sx={{ + flex: '1 1 0', + minWidth: 80, + p: 1.5, + textAlign: 'center', + cursor: 'pointer', + border: isSelected ? '2px solid #2980b9' : '2px solid transparent', + bgcolor: isSelected ? '#e3f2fd' : '#fff', + transition: 'all 0.15s', + '&:hover': { bgcolor: isSelected ? '#e3f2fd' : '#f5f5f5' } + }} + > + + {day.toLocaleDateString(intl.locale, { weekday: 'short' })} + + + {day.getDate()} + + + {loading + ? '...' + : selfSuff != null + ? `${selfSuff.toFixed(0)}%` + : '—'} + + + ); + })} + + + + + + ); +} + +// ── IntradayChart ──────────────────────────────────────────── + +const HOUR_LABELS = Array.from({ length: 24 }, (_, i) => + `${String(i).padStart(2, '0')}:00` +); + +function IntradayChart({ + hourlyData, + loading +}: { + hourlyData: HourlyEnergyRecord[]; + loading: boolean; +}) { + const intl = useIntl(); + + if (loading) { + return ( + + + + ); + } + + if (hourlyData.length === 0) { + return ( + + + + ); + } + + const hourMap = new Map(hourlyData.map((h) => [h.hour, h])); + + const pvData = HOUR_LABELS.map((_, i) => hourMap.get(i)?.pvKwh ?? null); + const loadData = HOUR_LABELS.map((_, i) => hourMap.get(i)?.loadKwh ?? null); + const batteryData = HOUR_LABELS.map((_, i) => { + const h = hourMap.get(i); + return h ? h.batteryDischargedKwh - h.batteryChargedKwh : null; + }); + const socData = HOUR_LABELS.map((_, i) => hourMap.get(i)?.battSoC ?? null); + + const chartData = { + labels: HOUR_LABELS, + datasets: [ + { + label: intl.formatMessage({ id: 'pvProduction', defaultMessage: 'PV Production' }), + data: pvData, + borderColor: '#f1c40f', + backgroundColor: 'rgba(241,196,15,0.1)', + fill: true, + tension: 0.3, + pointRadius: 2, + yAxisID: 'y' + }, + { + label: intl.formatMessage({ id: 'consumption', defaultMessage: 'Consumption' }), + data: loadData, + borderColor: '#e74c3c', + backgroundColor: 'rgba(231,76,60,0.1)', + fill: true, + tension: 0.3, + pointRadius: 2, + yAxisID: 'y' + }, + { + label: intl.formatMessage({ id: 'batteryPower', defaultMessage: 'Battery Power' }), + data: batteryData, + borderColor: '#3498db', + backgroundColor: 'rgba(52,152,219,0.1)', + fill: true, + tension: 0.3, + pointRadius: 2, + yAxisID: 'y' + }, + { + label: intl.formatMessage({ id: 'batterySoCLabel', defaultMessage: 'Battery SoC' }), + data: socData, + borderColor: '#27ae60', + borderDash: [6, 3], + backgroundColor: 'transparent', + tension: 0.3, + pointRadius: 2, + yAxisID: 'soc' + } + ] + }; + + const chartOptions = { + responsive: true, + maintainAspectRatio: false, + interaction: { mode: 'index' as const, intersect: false }, + plugins: { + legend: { position: 'top' as const } + }, + scales: { + y: { + position: 'left' as const, + title: { + display: true, + text: intl.formatMessage({ id: 'powerKw', defaultMessage: 'Power (kW)' }) + } + }, + soc: { + position: 'right' as const, + min: 0, + max: 100, + title: { + display: true, + text: intl.formatMessage({ id: 'socPercent', defaultMessage: 'SoC (%)' }) + }, + grid: { drawOnChartArea: false } + } + } + }; + + return ( + + + + + + + + + ); +} + +// ── DailySummaryTable ──────────────────────────────────────── + +function DailySummaryTable({ record }: { record: DailyEnergyData }) { + return ( + + + + + + + + + + + + + + + + + + + + {record.pvProduction.toFixed(1)} kWh + + + + + + {record.loadConsumption.toFixed(1)} kWh + + + + + + {record.gridImport.toFixed(1)} kWh + + + + + + {record.gridExport.toFixed(1)} kWh + + + + + + + + + + {record.batteryCharged.toFixed(1)} kWh + + + + + + + + + + {record.batteryDischarged.toFixed(1)} kWh + + + + + + ); +} diff --git a/typescript/frontend-marios2/src/content/dashboards/SodiohomeInstallations/WeeklyReport.tsx b/typescript/frontend-marios2/src/content/dashboards/SodiohomeInstallations/WeeklyReport.tsx index 092bd382b..b5cc1eae4 100644 --- a/typescript/frontend-marios2/src/content/dashboards/SodiohomeInstallations/WeeklyReport.tsx +++ b/typescript/frontend-marios2/src/content/dashboards/SodiohomeInstallations/WeeklyReport.tsx @@ -23,6 +23,7 @@ import DownloadIcon from '@mui/icons-material/Download'; import RefreshIcon from '@mui/icons-material/Refresh'; import ExpandMoreIcon from '@mui/icons-material/ExpandMore'; import axiosConfig from 'src/Resources/axiosConfig'; +import DailySection from './DailySection'; interface WeeklyReportProps { installationId: number; @@ -346,163 +347,6 @@ function WeeklyReport({ installationId }: WeeklyReportProps) { ); } -// ── Daily Section ────────────────────────────────────────────── - -function DailySection({ installationId }: { installationId: number }) { - const intl = useIntl(); - const yesterday = new Date(); - yesterday.setDate(yesterday.getDate() - 1); - const formatDate = (d: Date) => d.toISOString().split('T')[0]; - - const [selectedDate, setSelectedDate] = useState(formatDate(yesterday)); - const [dailyRecords, setDailyRecords] = useState([]); - const [loading, setLoading] = useState(false); - const [noData, setNoData] = useState(false); - - const fetchDailyData = async (date: string) => { - setLoading(true); - setNoData(false); - try { - const res = await axiosConfig.get('/GetDailyRecords', { - params: { installationId, from: date, to: date } - }); - const records = res.data?.records ?? res.data ?? []; - if (Array.isArray(records) && records.length > 0) { - setDailyRecords(records); - } else { - setDailyRecords([]); - setNoData(true); - } - } catch { - setDailyRecords([]); - setNoData(true); - } finally { - setLoading(false); - } - }; - - useEffect(() => { - fetchDailyData(selectedDate); - }, [installationId]); - - const handleDateChange = (e: React.ChangeEvent) => { - const newDate = e.target.value; - setSelectedDate(newDate); - fetchDailyData(newDate); - }; - - const record = dailyRecords.length > 0 ? dailyRecords[0] : null; - - const energySaved = record ? Math.max(0, record.loadConsumption - record.gridImport) : 0; - const savingsCHF = +(energySaved * 0.39).toFixed(2); - const selfSufficiency = record && record.loadConsumption > 0 - ? Math.min(100, ((1 - record.gridImport / record.loadConsumption) * 100)) - : 0; - const batteryEfficiency = record && record.batteryCharged > 0 - ? Math.min(100, (record.batteryDischarged / record.batteryCharged) * 100) - : 0; - - const dt = new Date(selectedDate); - const dateLabel = dt.toLocaleDateString(intl.locale, { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' }); - - return ( - <> - {/* Date Picker */} - - - - - - - - {loading && ( - - - - )} - - {!loading && noData && ( - - - - )} - - {!loading && record && ( - <> - {/* Header */} - - - - - - {dateLabel} - - - - {/* Savings Cards */} - - - - - {/* Daily Summary Table */} - - - - - - - - - - - - - - - {record.pvProduction.toFixed(1)} kWh - - - - {record.loadConsumption.toFixed(1)} kWh - - - - {record.gridImport.toFixed(1)} kWh - - - - {record.gridExport.toFixed(1)} kWh - - - - {record.batteryCharged.toFixed(1)} kWh - - - - {record.batteryDischarged.toFixed(1)} kWh - - - - - - )} - - ); -} - // ── Weekly Section (existing weekly report content) ──────────── function WeeklySection({ installationId, latestMonthlyPeriodEnd }: { installationId: number; latestMonthlyPeriodEnd: string | null }) { @@ -1210,7 +1054,7 @@ function InsightBox({ text, bullets }: { text: string; bullets: string[] }) { ); } -function SavingsCards({ intl, energySaved, savingsCHF, selfSufficiency, batteryEfficiency, hint }: { +export function SavingsCards({ intl, energySaved, savingsCHF, selfSufficiency, batteryEfficiency, hint }: { intl: any; energySaved: number; savingsCHF: number; diff --git a/typescript/frontend-marios2/src/lang/de.json b/typescript/frontend-marios2/src/lang/de.json index 8361c146b..e1bb76518 100644 --- a/typescript/frontend-marios2/src/lang/de.json +++ b/typescript/frontend-marios2/src/lang/de.json @@ -145,6 +145,13 @@ "dailySummary": "Tagesübersicht", "selectDate": "Datum wählen", "noDataForDate": "Keine Daten für das gewählte Datum verfügbar.", + "noHourlyData": "Stündliche Daten für diesen Tag nicht verfügbar.", + "dataUpTo": "Daten bis {date}", + "intradayChart": "Tagesverlauf Energiefluss", + "batteryPower": "Batterieleistung", + "batterySoCLabel": "Batterie SoC", + "powerKw": "Leistung (kW)", + "socPercent": "SoC (%)", "batteryActivity": "Batterieaktivität", "batteryCharged": "Batterie geladen", "batteryDischarged": "Batterie entladen", diff --git a/typescript/frontend-marios2/src/lang/en.json b/typescript/frontend-marios2/src/lang/en.json index 65adae667..dc98ed539 100644 --- a/typescript/frontend-marios2/src/lang/en.json +++ b/typescript/frontend-marios2/src/lang/en.json @@ -127,6 +127,13 @@ "dailySummary": "Daily Summary", "selectDate": "Select Date", "noDataForDate": "No data available for the selected date.", + "noHourlyData": "Hourly data not available for this day.", + "dataUpTo": "Data up to {date}", + "intradayChart": "Intraday Power Flow", + "batteryPower": "Battery Power", + "batterySoCLabel": "Battery SoC", + "powerKw": "Power (kW)", + "socPercent": "SoC (%)", "batteryActivity": "Battery Activity", "batteryCharged": "Battery Charged", "batteryDischarged": "Battery Discharged", diff --git a/typescript/frontend-marios2/src/lang/fr.json b/typescript/frontend-marios2/src/lang/fr.json index 8a699b1fc..4a905fd80 100644 --- a/typescript/frontend-marios2/src/lang/fr.json +++ b/typescript/frontend-marios2/src/lang/fr.json @@ -139,6 +139,13 @@ "dailySummary": "Résumé du jour", "selectDate": "Sélectionner la date", "noDataForDate": "Aucune donnée disponible pour la date sélectionnée.", + "noHourlyData": "Données horaires non disponibles pour ce jour.", + "dataUpTo": "Données jusqu'au {date}", + "intradayChart": "Flux d'énergie journalier", + "batteryPower": "Puissance batterie", + "batterySoCLabel": "SoC batterie", + "powerKw": "Puissance (kW)", + "socPercent": "SoC (%)", "batteryActivity": "Activité de la batterie", "batteryCharged": "Batterie chargée", "batteryDischarged": "Batterie déchargée", diff --git a/typescript/frontend-marios2/src/lang/it.json b/typescript/frontend-marios2/src/lang/it.json index 1311b6681..0ca4aac38 100644 --- a/typescript/frontend-marios2/src/lang/it.json +++ b/typescript/frontend-marios2/src/lang/it.json @@ -150,6 +150,13 @@ "dailySummary": "Riepilogo del giorno", "selectDate": "Seleziona data", "noDataForDate": "Nessun dato disponibile per la data selezionata.", + "noHourlyData": "Dati orari non disponibili per questo giorno.", + "dataUpTo": "Dati fino al {date}", + "intradayChart": "Flusso energetico giornaliero", + "batteryPower": "Potenza batteria", + "batterySoCLabel": "SoC batteria", + "powerKw": "Potenza (kW)", + "socPercent": "SoC (%)", "batteryActivity": "Attività della batteria", "batteryCharged": "Batteria caricata", "batteryDischarged": "Batteria scaricata",