daily tab design with hourly data and last week quick entry with self-efficiency on top
This commit is contained in:
parent
0ac22ecbe9
commit
6cf14e3483
|
|
@ -1235,6 +1235,119 @@ public class Controller : ControllerBase
|
||||||
return Ok(new { count = records.Count, records });
|
return Ok(new { count = records.Count, records });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[HttpGet(nameof(GetHourlyRecords))]
|
||||||
|
public ActionResult<List<HourlyEnergyRecord>> 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 });
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Returns daily + hourly records for a date range.
|
||||||
|
/// DB first; if empty, falls back to xlsx parsing and caches results for future calls.
|
||||||
|
/// </summary>
|
||||||
|
[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 },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Deletes DailyEnergyRecord rows for an installation in the given date range.
|
/// Deletes DailyEnergyRecord rows for an installation in the given date range.
|
||||||
/// Safe to use during testing — only removes daily records, not report summaries.
|
/// Safe to use during testing — only removes daily records, not report summaries.
|
||||||
|
|
|
||||||
|
|
@ -116,13 +116,9 @@ public static partial class Db
|
||||||
{
|
{
|
||||||
var fromStr = from.ToString("yyyy-MM-dd");
|
var fromStr = from.ToString("yyyy-MM-dd");
|
||||||
var toStr = to.ToString("yyyy-MM-dd");
|
var toStr = to.ToString("yyyy-MM-dd");
|
||||||
return DailyRecords
|
return Connection.Query<DailyEnergyRecord>(
|
||||||
.Where(r => r.InstallationId == installationId)
|
"SELECT * FROM DailyEnergyRecord WHERE InstallationId = ? AND Date >= ? AND Date <= ? ORDER BY Date",
|
||||||
.ToList()
|
installationId, fromStr, toStr);
|
||||||
.Where(r => String.Compare(r.Date, fromStr, StringComparison.Ordinal) >= 0
|
|
||||||
&& String.Compare(r.Date, toStr, StringComparison.Ordinal) <= 0)
|
|
||||||
.OrderBy(r => r.Date)
|
|
||||||
.ToList();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
@ -141,13 +137,9 @@ public static partial class Db
|
||||||
{
|
{
|
||||||
var fromStr = from.ToString("yyyy-MM-dd");
|
var fromStr = from.ToString("yyyy-MM-dd");
|
||||||
var toStr = to.ToString("yyyy-MM-dd");
|
var toStr = to.ToString("yyyy-MM-dd");
|
||||||
return HourlyRecords
|
return Connection.Query<HourlyEnergyRecord>(
|
||||||
.Where(r => r.InstallationId == installationId)
|
"SELECT * FROM HourlyEnergyRecord WHERE InstallationId = ? AND Date >= ? AND Date <= ? ORDER BY Date, Hour",
|
||||||
.ToList()
|
installationId, fromStr, toStr);
|
||||||
.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();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,7 @@ public static class WeeklyReportService
|
||||||
/// Filename pattern: {installationId}_MMDD_MMDD.xlsx (e.g. "848_0302_0308.xlsx")
|
/// Filename pattern: {installationId}_MMDD_MMDD.xlsx (e.g. "848_0302_0308.xlsx")
|
||||||
/// Falls back to all files if filenames can't be parsed.
|
/// Falls back to all files if filenames can't be parsed.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private static List<string> GetRelevantXlsxFiles(long installationId, DateOnly rangeStart, DateOnly rangeEnd)
|
public static List<string> GetRelevantXlsxFiles(long installationId, DateOnly rangeStart, DateOnly rangeEnd)
|
||||||
{
|
{
|
||||||
if (!Directory.Exists(TmpReportDir))
|
if (!Directory.Exists(TmpReportDir))
|
||||||
return new List<string>();
|
return new List<string>();
|
||||||
|
|
|
||||||
|
|
@ -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<DailyEnergyData[]>([]);
|
||||||
|
const [weekHourlyRecords, setWeekHourlyRecords] = useState<HourlyEnergyRecord[]>([]);
|
||||||
|
const [selectedDate, setSelectedDate] = useState(formatDateISO(sunday));
|
||||||
|
const [selectedDayRecord, setSelectedDayRecord] = useState<DailyEnergyData | null>(null);
|
||||||
|
const [hourlyRecords, setHourlyRecords] = useState<HourlyEnergyRecord[]>([]);
|
||||||
|
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<HTMLInputElement>) => {
|
||||||
|
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 */}
|
||||||
|
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2, mb: 2 }}>
|
||||||
|
<Typography variant="body1" fontWeight="bold">
|
||||||
|
<FormattedMessage id="selectDate" defaultMessage="Select Date" />
|
||||||
|
</Typography>
|
||||||
|
<TextField
|
||||||
|
type="date"
|
||||||
|
size="small"
|
||||||
|
value={selectedDate}
|
||||||
|
onChange={handleDatePicker}
|
||||||
|
inputProps={{ max: formatDateISO(new Date()) }}
|
||||||
|
sx={{ width: 200 }}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{/* 7-Day Strip */}
|
||||||
|
<DayStrip
|
||||||
|
weekDays={weekDays}
|
||||||
|
weekRecords={weekRecords}
|
||||||
|
selectedDate={selectedDate}
|
||||||
|
onSelect={handleStripSelect}
|
||||||
|
sunday={sunday}
|
||||||
|
loading={loadingWeek}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Loading state */}
|
||||||
|
{loadingWeek && !record && (
|
||||||
|
<Container
|
||||||
|
sx={{
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'center',
|
||||||
|
alignItems: 'center',
|
||||||
|
height: '20vh'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<CircularProgress size={40} style={{ color: '#ffc04d' }} />
|
||||||
|
</Container>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* No data state */}
|
||||||
|
{!loadingWeek && noData && !record && (
|
||||||
|
<Alert severity="info" sx={{ mb: 3 }}>
|
||||||
|
<FormattedMessage
|
||||||
|
id="noDataForDate"
|
||||||
|
defaultMessage="No data available for the selected date."
|
||||||
|
/>
|
||||||
|
</Alert>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Day detail */}
|
||||||
|
{record && (
|
||||||
|
<>
|
||||||
|
{/* Header */}
|
||||||
|
<Paper
|
||||||
|
sx={{ bgcolor: '#2c3e50', color: '#fff', p: 3, mb: 3, borderRadius: 2 }}
|
||||||
|
>
|
||||||
|
<Typography variant="h5" fontWeight="bold">
|
||||||
|
<FormattedMessage
|
||||||
|
id="dailyReportTitle"
|
||||||
|
defaultMessage="Daily Energy Summary"
|
||||||
|
/>
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="body2" sx={{ opacity: 0.8, mt: 0.5 }}>
|
||||||
|
{dateLabel}
|
||||||
|
</Typography>
|
||||||
|
</Paper>
|
||||||
|
|
||||||
|
{/* KPI Cards */}
|
||||||
|
<Paper sx={{ p: 3, mb: 3 }}>
|
||||||
|
<SavingsCards
|
||||||
|
intl={intl}
|
||||||
|
energySaved={+kpis.energySaved.toFixed(1)}
|
||||||
|
savingsCHF={kpis.savingsCHF}
|
||||||
|
selfSufficiency={kpis.selfSufficiency}
|
||||||
|
batteryEfficiency={kpis.batteryEfficiency}
|
||||||
|
/>
|
||||||
|
</Paper>
|
||||||
|
|
||||||
|
{/* Intraday Chart */}
|
||||||
|
<IntradayChart
|
||||||
|
hourlyData={hourlyRecords}
|
||||||
|
loading={loadingWeek}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Summary Table */}
|
||||||
|
<DailySummaryTable record={record} />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── 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 (
|
||||||
|
<Box sx={{ mb: 3 }}>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: 'flex',
|
||||||
|
gap: 1,
|
||||||
|
overflowX: 'auto',
|
||||||
|
pb: 1,
|
||||||
|
mb: 1
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{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 (
|
||||||
|
<Paper
|
||||||
|
key={dateStr}
|
||||||
|
onClick={() => 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' }
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Typography variant="caption" fontWeight="bold" sx={{ color: '#666' }}>
|
||||||
|
{day.toLocaleDateString(intl.locale, { weekday: 'short' })}
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="h6" fontWeight="bold" sx={{ lineHeight: 1.2 }}>
|
||||||
|
{day.getDate()}
|
||||||
|
</Typography>
|
||||||
|
<Typography
|
||||||
|
variant="caption"
|
||||||
|
sx={{ color: selfSuff != null ? '#8e44ad' : '#ccc' }}
|
||||||
|
>
|
||||||
|
{loading
|
||||||
|
? '...'
|
||||||
|
: selfSuff != null
|
||||||
|
? `${selfSuff.toFixed(0)}%`
|
||||||
|
: '—'}
|
||||||
|
</Typography>
|
||||||
|
</Paper>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</Box>
|
||||||
|
<Typography variant="caption" sx={{ color: '#888' }}>
|
||||||
|
<FormattedMessage
|
||||||
|
id="dataUpTo"
|
||||||
|
defaultMessage="Data up to {date}"
|
||||||
|
values={{ date: sundayLabel }}
|
||||||
|
/>
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── 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 (
|
||||||
|
<Box sx={{ display: 'flex', justifyContent: 'center', py: 4, mb: 3 }}>
|
||||||
|
<CircularProgress size={30} style={{ color: '#ffc04d' }} />
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hourlyData.length === 0) {
|
||||||
|
return (
|
||||||
|
<Alert severity="info" sx={{ mb: 3 }}>
|
||||||
|
<FormattedMessage
|
||||||
|
id="noHourlyData"
|
||||||
|
defaultMessage="Hourly data not available for this day."
|
||||||
|
/>
|
||||||
|
</Alert>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
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 (
|
||||||
|
<Paper sx={{ p: 3, mb: 3 }}>
|
||||||
|
<Typography variant="h6" fontWeight="bold" sx={{ mb: 2, color: '#2c3e50' }}>
|
||||||
|
<FormattedMessage
|
||||||
|
id="intradayChart"
|
||||||
|
defaultMessage="Intraday Power Flow"
|
||||||
|
/>
|
||||||
|
</Typography>
|
||||||
|
<Box sx={{ height: 350 }}>
|
||||||
|
<Line data={chartData} options={chartOptions} />
|
||||||
|
</Box>
|
||||||
|
</Paper>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── DailySummaryTable ────────────────────────────────────────
|
||||||
|
|
||||||
|
function DailySummaryTable({ record }: { record: DailyEnergyData }) {
|
||||||
|
return (
|
||||||
|
<Paper sx={{ p: 3, mb: 3 }}>
|
||||||
|
<Typography variant="h6" fontWeight="bold" sx={{ mb: 2, color: '#2c3e50' }}>
|
||||||
|
<FormattedMessage id="dailySummary" defaultMessage="Daily Summary" />
|
||||||
|
</Typography>
|
||||||
|
<Box
|
||||||
|
component="table"
|
||||||
|
sx={{
|
||||||
|
width: '100%',
|
||||||
|
borderCollapse: 'collapse',
|
||||||
|
'& td, & th': { p: 1.5, borderBottom: '1px solid #eee' }
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<thead>
|
||||||
|
<tr style={{ background: '#f8f9fa' }}>
|
||||||
|
<th style={{ textAlign: 'left' }}>
|
||||||
|
<FormattedMessage id="metric" defaultMessage="Metric" />
|
||||||
|
</th>
|
||||||
|
<th style={{ textAlign: 'right' }}>
|
||||||
|
<FormattedMessage id="total" defaultMessage="Total" />
|
||||||
|
</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td><FormattedMessage id="pvProduction" defaultMessage="PV Production" /></td>
|
||||||
|
<td style={{ textAlign: 'right', fontWeight: 'bold' }}>
|
||||||
|
{record.pvProduction.toFixed(1)} kWh
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><FormattedMessage id="consumption" defaultMessage="Consumption" /></td>
|
||||||
|
<td style={{ textAlign: 'right', fontWeight: 'bold' }}>
|
||||||
|
{record.loadConsumption.toFixed(1)} kWh
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><FormattedMessage id="gridImport" defaultMessage="Grid Import" /></td>
|
||||||
|
<td style={{ textAlign: 'right', fontWeight: 'bold' }}>
|
||||||
|
{record.gridImport.toFixed(1)} kWh
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><FormattedMessage id="gridExport" defaultMessage="Grid Export" /></td>
|
||||||
|
<td style={{ textAlign: 'right', fontWeight: 'bold' }}>
|
||||||
|
{record.gridExport.toFixed(1)} kWh
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr style={{ background: '#f0f7ff' }}>
|
||||||
|
<td>
|
||||||
|
<strong>
|
||||||
|
<FormattedMessage id="batteryCharged" defaultMessage="Battery Charged" />
|
||||||
|
</strong>
|
||||||
|
</td>
|
||||||
|
<td style={{ textAlign: 'right', fontWeight: 'bold', color: '#2980b9' }}>
|
||||||
|
{record.batteryCharged.toFixed(1)} kWh
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr style={{ background: '#f0f7ff' }}>
|
||||||
|
<td>
|
||||||
|
<strong>
|
||||||
|
<FormattedMessage
|
||||||
|
id="batteryDischarged"
|
||||||
|
defaultMessage="Battery Discharged"
|
||||||
|
/>
|
||||||
|
</strong>
|
||||||
|
</td>
|
||||||
|
<td style={{ textAlign: 'right', fontWeight: 'bold', color: '#e67e22' }}>
|
||||||
|
{record.batteryDischarged.toFixed(1)} kWh
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</Box>
|
||||||
|
</Paper>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -23,6 +23,7 @@ import DownloadIcon from '@mui/icons-material/Download';
|
||||||
import RefreshIcon from '@mui/icons-material/Refresh';
|
import RefreshIcon from '@mui/icons-material/Refresh';
|
||||||
import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
|
import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
|
||||||
import axiosConfig from 'src/Resources/axiosConfig';
|
import axiosConfig from 'src/Resources/axiosConfig';
|
||||||
|
import DailySection from './DailySection';
|
||||||
|
|
||||||
interface WeeklyReportProps {
|
interface WeeklyReportProps {
|
||||||
installationId: number;
|
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<DailyEnergyData[]>([]);
|
|
||||||
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<HTMLInputElement>) => {
|
|
||||||
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 */}
|
|
||||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2, mb: 3 }}>
|
|
||||||
<Typography variant="body1" fontWeight="bold">
|
|
||||||
<FormattedMessage id="selectDate" defaultMessage="Select Date" />
|
|
||||||
</Typography>
|
|
||||||
<TextField
|
|
||||||
type="date"
|
|
||||||
size="small"
|
|
||||||
value={selectedDate}
|
|
||||||
onChange={handleDateChange}
|
|
||||||
inputProps={{ max: formatDate(new Date()) }}
|
|
||||||
sx={{ width: 200 }}
|
|
||||||
/>
|
|
||||||
</Box>
|
|
||||||
|
|
||||||
{loading && (
|
|
||||||
<Container sx={{ display: 'flex', flexDirection: 'column', justifyContent: 'center', alignItems: 'center', height: '30vh' }}>
|
|
||||||
<CircularProgress size={50} style={{ color: '#ffc04d' }} />
|
|
||||||
</Container>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{!loading && noData && (
|
|
||||||
<Alert severity="info" sx={{ mb: 3 }}>
|
|
||||||
<FormattedMessage id="noDataForDate" defaultMessage="No data available for the selected date." />
|
|
||||||
</Alert>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{!loading && record && (
|
|
||||||
<>
|
|
||||||
{/* Header */}
|
|
||||||
<Paper sx={{ bgcolor: '#2c3e50', color: '#fff', p: 3, mb: 3, borderRadius: 2 }}>
|
|
||||||
<Typography variant="h5" fontWeight="bold">
|
|
||||||
<FormattedMessage id="dailyReportTitle" defaultMessage="Daily Energy Summary" />
|
|
||||||
</Typography>
|
|
||||||
<Typography variant="body2" sx={{ opacity: 0.8, mt: 0.5 }}>
|
|
||||||
{dateLabel}
|
|
||||||
</Typography>
|
|
||||||
</Paper>
|
|
||||||
|
|
||||||
{/* Savings Cards */}
|
|
||||||
<Paper sx={{ p: 3, mb: 3 }}>
|
|
||||||
<SavingsCards
|
|
||||||
intl={intl}
|
|
||||||
energySaved={+energySaved.toFixed(1)}
|
|
||||||
savingsCHF={savingsCHF}
|
|
||||||
selfSufficiency={selfSufficiency}
|
|
||||||
batteryEfficiency={batteryEfficiency}
|
|
||||||
/>
|
|
||||||
</Paper>
|
|
||||||
|
|
||||||
{/* Daily Summary Table */}
|
|
||||||
<Paper sx={{ p: 3, mb: 3 }}>
|
|
||||||
<Typography variant="h6" fontWeight="bold" sx={{ mb: 2, color: '#2c3e50' }}>
|
|
||||||
<FormattedMessage id="dailySummary" defaultMessage="Daily Summary" />
|
|
||||||
</Typography>
|
|
||||||
<Box component="table" sx={{ width: '100%', borderCollapse: 'collapse', '& td, & th': { p: 1.5, borderBottom: '1px solid #eee' } }}>
|
|
||||||
<thead>
|
|
||||||
<tr style={{ background: '#f8f9fa' }}>
|
|
||||||
<th style={{ textAlign: 'left' }}><FormattedMessage id="metric" defaultMessage="Metric" /></th>
|
|
||||||
<th style={{ textAlign: 'right' }}><FormattedMessage id="total" defaultMessage="Total" /></th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
<tr>
|
|
||||||
<td><FormattedMessage id="pvProduction" defaultMessage="PV Production" /></td>
|
|
||||||
<td style={{ textAlign: 'right', fontWeight: 'bold' }}>{record.pvProduction.toFixed(1)} kWh</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td><FormattedMessage id="consumption" defaultMessage="Consumption" /></td>
|
|
||||||
<td style={{ textAlign: 'right', fontWeight: 'bold' }}>{record.loadConsumption.toFixed(1)} kWh</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td><FormattedMessage id="gridImport" defaultMessage="Grid Import" /></td>
|
|
||||||
<td style={{ textAlign: 'right', fontWeight: 'bold' }}>{record.gridImport.toFixed(1)} kWh</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td><FormattedMessage id="gridExport" defaultMessage="Grid Export" /></td>
|
|
||||||
<td style={{ textAlign: 'right', fontWeight: 'bold' }}>{record.gridExport.toFixed(1)} kWh</td>
|
|
||||||
</tr>
|
|
||||||
<tr style={{ background: '#f0f7ff' }}>
|
|
||||||
<td><strong><FormattedMessage id="batteryCharged" defaultMessage="Battery Charged" /></strong></td>
|
|
||||||
<td style={{ textAlign: 'right', fontWeight: 'bold', color: '#2980b9' }}>{record.batteryCharged.toFixed(1)} kWh</td>
|
|
||||||
</tr>
|
|
||||||
<tr style={{ background: '#f0f7ff' }}>
|
|
||||||
<td><strong><FormattedMessage id="batteryDischarged" defaultMessage="Battery Discharged" /></strong></td>
|
|
||||||
<td style={{ textAlign: 'right', fontWeight: 'bold', color: '#e67e22' }}>{record.batteryDischarged.toFixed(1)} kWh</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</Box>
|
|
||||||
</Paper>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── Weekly Section (existing weekly report content) ────────────
|
// ── Weekly Section (existing weekly report content) ────────────
|
||||||
|
|
||||||
function WeeklySection({ installationId, latestMonthlyPeriodEnd }: { installationId: number; latestMonthlyPeriodEnd: string | null }) {
|
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;
|
intl: any;
|
||||||
energySaved: number;
|
energySaved: number;
|
||||||
savingsCHF: number;
|
savingsCHF: number;
|
||||||
|
|
|
||||||
|
|
@ -145,6 +145,13 @@
|
||||||
"dailySummary": "Tagesübersicht",
|
"dailySummary": "Tagesübersicht",
|
||||||
"selectDate": "Datum wählen",
|
"selectDate": "Datum wählen",
|
||||||
"noDataForDate": "Keine Daten für das gewählte Datum verfügbar.",
|
"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",
|
"batteryActivity": "Batterieaktivität",
|
||||||
"batteryCharged": "Batterie geladen",
|
"batteryCharged": "Batterie geladen",
|
||||||
"batteryDischarged": "Batterie entladen",
|
"batteryDischarged": "Batterie entladen",
|
||||||
|
|
|
||||||
|
|
@ -127,6 +127,13 @@
|
||||||
"dailySummary": "Daily Summary",
|
"dailySummary": "Daily Summary",
|
||||||
"selectDate": "Select Date",
|
"selectDate": "Select Date",
|
||||||
"noDataForDate": "No data available for the selected 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",
|
"batteryActivity": "Battery Activity",
|
||||||
"batteryCharged": "Battery Charged",
|
"batteryCharged": "Battery Charged",
|
||||||
"batteryDischarged": "Battery Discharged",
|
"batteryDischarged": "Battery Discharged",
|
||||||
|
|
|
||||||
|
|
@ -139,6 +139,13 @@
|
||||||
"dailySummary": "Résumé du jour",
|
"dailySummary": "Résumé du jour",
|
||||||
"selectDate": "Sélectionner la date",
|
"selectDate": "Sélectionner la date",
|
||||||
"noDataForDate": "Aucune donnée disponible pour la date sélectionnée.",
|
"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",
|
"batteryActivity": "Activité de la batterie",
|
||||||
"batteryCharged": "Batterie chargée",
|
"batteryCharged": "Batterie chargée",
|
||||||
"batteryDischarged": "Batterie déchargée",
|
"batteryDischarged": "Batterie déchargée",
|
||||||
|
|
|
||||||
|
|
@ -150,6 +150,13 @@
|
||||||
"dailySummary": "Riepilogo del giorno",
|
"dailySummary": "Riepilogo del giorno",
|
||||||
"selectDate": "Seleziona data",
|
"selectDate": "Seleziona data",
|
||||||
"noDataForDate": "Nessun dato disponibile per la data selezionata.",
|
"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",
|
"batteryActivity": "Attività della batteria",
|
||||||
"batteryCharged": "Batteria caricata",
|
"batteryCharged": "Batteria caricata",
|
||||||
"batteryDischarged": "Batteria scaricata",
|
"batteryDischarged": "Batteria scaricata",
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue