Compare commits
No commits in common. "897f3137f5db87d7cfe0d557415f9b02d7c08dce" and "35938e95976ad820058714c55b8851850c5efd8c" have entirely different histories.
897f3137f5
...
35938e9597
|
|
@ -1471,91 +1471,6 @@ public class Controller : ControllerBase
|
|||
}
|
||||
}
|
||||
|
||||
// ── Report HTML (for PDF download) ─────────────────────────────
|
||||
|
||||
[HttpGet(nameof(GetWeeklyReportHtml))]
|
||||
public async Task<ActionResult> GetWeeklyReportHtml(Int64 installationId, Token authToken, String? language = null)
|
||||
{
|
||||
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();
|
||||
|
||||
var lang = language ?? user.Language ?? "en";
|
||||
var report = await WeeklyReportService.GenerateReportAsync(installationId, installation.Name, lang);
|
||||
var html = ReportEmailService.BuildHtmlEmail(report, lang);
|
||||
return Content(html, "text/html");
|
||||
}
|
||||
|
||||
[HttpGet(nameof(GetMonthlyReportHtml))]
|
||||
public async Task<ActionResult> GetMonthlyReportHtml(Int64 installationId, Int32 year, Int32 month, Token authToken, String? language = null)
|
||||
{
|
||||
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();
|
||||
|
||||
var lang = language ?? user.Language ?? "en";
|
||||
var report = Db.GetMonthlyReports(installationId).FirstOrDefault(r => r.Year == year && r.Month == month);
|
||||
if (report == null) return BadRequest($"No monthly report found for {year}-{month:D2}.");
|
||||
|
||||
report.AiInsight = await ReportAggregationService.GetOrGenerateMonthlyInsightAsync(report, lang);
|
||||
var s = ReportEmailService.GetAggregatedStrings(lang, "monthly");
|
||||
var html = ReportEmailService.BuildAggregatedHtmlEmail(
|
||||
report.PeriodStart, report.PeriodEnd, installation.Name,
|
||||
report.TotalPvProduction, report.TotalConsumption, report.TotalGridImport, report.TotalGridExport,
|
||||
report.TotalBatteryCharged, report.TotalBatteryDischarged, report.TotalEnergySaved, report.TotalSavingsCHF,
|
||||
report.SelfSufficiencyPercent, report.BatteryEfficiencyPercent, report.AiInsight,
|
||||
$"{report.WeekCount} {s.CountLabel}", s);
|
||||
return Content(html, "text/html");
|
||||
}
|
||||
|
||||
[HttpGet(nameof(GetYearlyReportHtml))]
|
||||
public async Task<ActionResult> GetYearlyReportHtml(Int64 installationId, Int32 year, Token authToken, String? language = null)
|
||||
{
|
||||
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();
|
||||
|
||||
var lang = language ?? user.Language ?? "en";
|
||||
var report = Db.GetYearlyReports(installationId).FirstOrDefault(r => r.Year == year);
|
||||
if (report == null) return BadRequest($"No yearly report found for {year}.");
|
||||
|
||||
report.AiInsight = await ReportAggregationService.GetOrGenerateYearlyInsightAsync(report, lang);
|
||||
var s = ReportEmailService.GetAggregatedStrings(lang, "yearly");
|
||||
var html = ReportEmailService.BuildAggregatedHtmlEmail(
|
||||
report.PeriodStart, report.PeriodEnd, installation.Name,
|
||||
report.TotalPvProduction, report.TotalConsumption, report.TotalGridImport, report.TotalGridExport,
|
||||
report.TotalBatteryCharged, report.TotalBatteryDischarged, report.TotalEnergySaved, report.TotalSavingsCHF,
|
||||
report.SelfSufficiencyPercent, report.BatteryEfficiencyPercent, report.AiInsight,
|
||||
$"{report.MonthCount} {s.CountLabel}", s);
|
||||
return Content(html, "text/html");
|
||||
}
|
||||
|
||||
[HttpGet(nameof(GetDailyReportHtml))]
|
||||
public ActionResult GetDailyReportHtml(Int64 installationId, String date, Token authToken, String? language = null)
|
||||
{
|
||||
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(date, "yyyy-MM-dd", out var parsedDate))
|
||||
return BadRequest("date must be in yyyy-MM-dd format.");
|
||||
|
||||
var records = Db.GetDailyRecords(installationId, parsedDate, parsedDate);
|
||||
if (records.Count == 0) return BadRequest($"No daily record found for {date}.");
|
||||
|
||||
var lang = language ?? user.Language ?? "en";
|
||||
var html = ReportEmailService.BuildDailyHtmlEmail(records[0], installation.Name, lang);
|
||||
return Content(html, "text/html");
|
||||
}
|
||||
|
||||
[HttpGet(nameof(GetWeeklyReportSummaries))]
|
||||
public async Task<ActionResult<List<WeeklyReportSummary>>> GetWeeklyReportSummaries(
|
||||
Int64 installationId, Int32 year, Int32 month, Token authToken, String? language = null)
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
Before Width: | Height: | Size: 6.2 KiB |
|
|
@ -53,50 +53,6 @@ function InformationSodistorehome(props: InformationSodistorehomeProps) {
|
|||
[]
|
||||
);
|
||||
|
||||
// Parse inverter/datalogger serial numbers from various legacy formats:
|
||||
// Slash-separated: "SN001/SN002"
|
||||
// Labeled comma: "Inverter 1: SN001, Inverter 2: SN002"
|
||||
// Labeled comma: "Datalogger 1: SN001, Datalogger 2: SN002"
|
||||
// Plain string: "SN001"
|
||||
const parseSerialNumbers = (value: string | undefined): string[] => {
|
||||
if (!value || value.trim() === '') return [];
|
||||
|
||||
// Check for labeled comma format: "Inverter 1: SN001, Inverter 2: SN002"
|
||||
if (/(?:Inverter|Datalogger)\s*\d+\s*:/i.test(value)) {
|
||||
const matches = value.match(/(?:Inverter|Datalogger)\s*\d+\s*:\s*([^,]+)/gi);
|
||||
if (matches) {
|
||||
return matches
|
||||
.map((m) => m.replace(/^(?:Inverter|Datalogger)\s*\d+\s*:\s*/i, '').trim())
|
||||
.filter((s) => s !== '');
|
||||
}
|
||||
}
|
||||
|
||||
// Slash-separated format: "SN001/SN002"
|
||||
if (value.includes('/')) {
|
||||
return value.split('/').filter((s) => s.trim() !== '');
|
||||
}
|
||||
|
||||
// Single value
|
||||
return [value.trim()];
|
||||
};
|
||||
|
||||
const [inverterNumber, setInverterNumber] = useState<number | ''>(() => {
|
||||
const parts = parseSerialNumbers(props.values.inverterSN);
|
||||
return parts.length > 0 ? parts.length : 1;
|
||||
});
|
||||
const [inverterSerialNumbers, setInverterSerialNumbers] = useState<string[]>(
|
||||
() => {
|
||||
const parts = parseSerialNumbers(props.values.inverterSN);
|
||||
return parts.length > 0 ? parts : [''];
|
||||
}
|
||||
);
|
||||
const [dataloggerSerialNumbers, setDataloggerSerialNumbers] = useState<string[]>(
|
||||
() => {
|
||||
const parts = parseSerialNumbers(props.values.dataloggerSN);
|
||||
return parts.length > 0 ? parts : [''];
|
||||
}
|
||||
);
|
||||
|
||||
const DeviceTypes = [
|
||||
{ id: 3, name: 'Growatt' },
|
||||
{ id: 4, name: 'Sinexcel' }
|
||||
|
|
@ -185,59 +141,6 @@ function InformationSodistorehome(props: InformationSodistorehomeProps) {
|
|||
}
|
||||
}
|
||||
};
|
||||
const handleInverterNumberChange = (e) => {
|
||||
const inputValue = e.target.value;
|
||||
// Allow empty while user is mid-edit
|
||||
if (inputValue === '') {
|
||||
setInverterNumber('');
|
||||
return;
|
||||
}
|
||||
if (/^\d+$/.test(inputValue)) {
|
||||
const value = Math.max(1, parseInt(inputValue));
|
||||
setInverterNumber(value);
|
||||
|
||||
const newInverterSNs = Array.from({ length: value }, (_, i) =>
|
||||
inverterSerialNumbers[i] || ''
|
||||
);
|
||||
const newDataloggerSNs = Array.from({ length: value }, (_, i) =>
|
||||
dataloggerSerialNumbers[i] || ''
|
||||
);
|
||||
setInverterSerialNumbers(newInverterSNs);
|
||||
setDataloggerSerialNumbers(newDataloggerSNs);
|
||||
setFormValues({
|
||||
...formValues,
|
||||
inverterSN: newInverterSNs.filter((s) => s !== '').join('/'),
|
||||
dataloggerSN: newDataloggerSNs.filter((s) => s !== '').join('/')
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handleInverterNumberBlur = () => {
|
||||
if (inverterNumber === '' || inverterNumber < 1) {
|
||||
setInverterNumber(1);
|
||||
}
|
||||
};
|
||||
|
||||
const handleInverterSerialNumberChange = (index: number, value: string) => {
|
||||
const updated = [...inverterSerialNumbers];
|
||||
updated[index] = value;
|
||||
setInverterSerialNumbers(updated);
|
||||
setFormValues({
|
||||
...formValues,
|
||||
inverterSN: updated.filter((s) => s !== '').join('/')
|
||||
});
|
||||
};
|
||||
|
||||
const handleDataloggerSerialNumberChange = (index: number, value: string) => {
|
||||
const updated = [...dataloggerSerialNumbers];
|
||||
updated[index] = value;
|
||||
setDataloggerSerialNumbers(updated);
|
||||
setFormValues({
|
||||
...formValues,
|
||||
dataloggerSN: updated.filter((s) => s !== '').join('/')
|
||||
});
|
||||
};
|
||||
|
||||
const handleSubmit = () => {
|
||||
setLoading(true);
|
||||
setError(false);
|
||||
|
|
@ -592,54 +495,35 @@ function InformationSodistorehome(props: InformationSodistorehomeProps) {
|
|||
<TextField
|
||||
label={
|
||||
<FormattedMessage
|
||||
id="inverterNumber"
|
||||
defaultMessage="Inverter Number"
|
||||
id="inverterSN"
|
||||
defaultMessage="Inverter Serial Number"
|
||||
/>
|
||||
}
|
||||
name="inverterNumber"
|
||||
type="text"
|
||||
value={inverterNumber}
|
||||
onChange={handleInverterNumberChange}
|
||||
onBlur={handleInverterNumberBlur}
|
||||
name="inverterSN"
|
||||
value={formValues.inverterSN}
|
||||
onChange={handleChange}
|
||||
variant="outlined"
|
||||
fullWidth
|
||||
inputProps={{ readOnly: !canEdit }}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{typeof inverterNumber === 'number' && inverterNumber > 0 &&
|
||||
inverterSerialNumbers.map((sn, index) => (
|
||||
<div key={`inv-${index}`}>
|
||||
<div>
|
||||
<TextField
|
||||
label={`Inverter Serial Number ${index + 1}`}
|
||||
name={`inverterSN${index + 1}`}
|
||||
value={sn}
|
||||
onChange={(e) =>
|
||||
handleInverterSerialNumberChange(index, e.target.value)
|
||||
label={
|
||||
<FormattedMessage
|
||||
id="dataloggerSN"
|
||||
defaultMessage="Datalogger Serial Number"
|
||||
/>
|
||||
}
|
||||
name="dataloggerSN"
|
||||
value={formValues.dataloggerSN}
|
||||
onChange={handleChange}
|
||||
variant="outlined"
|
||||
fullWidth
|
||||
inputProps={{ readOnly: !canEdit }}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
|
||||
{typeof inverterNumber === 'number' && inverterNumber > 0 &&
|
||||
dataloggerSerialNumbers.map((sn, index) => (
|
||||
<div key={`dl-${index}`}>
|
||||
<TextField
|
||||
label={`Datalogger Serial Number ${index + 1}`}
|
||||
name={`dataloggerSN${index + 1}`}
|
||||
value={sn}
|
||||
onChange={(e) =>
|
||||
handleDataloggerSerialNumberChange(index, e.target.value)
|
||||
}
|
||||
variant="outlined"
|
||||
fullWidth
|
||||
inputProps={{ readOnly: !canEdit }}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
|
||||
<div>
|
||||
<TextField
|
||||
|
|
|
|||
|
|
@ -97,12 +97,10 @@ function getCurrentWeekDays(currentMonday: Date): Date[] {
|
|||
|
||||
export default function DailySection({
|
||||
installationId,
|
||||
onHasData,
|
||||
onPeriodChange
|
||||
onHasData
|
||||
}: {
|
||||
installationId: number;
|
||||
onHasData?: (hasData: boolean) => void;
|
||||
onPeriodChange?: (date: string) => void;
|
||||
}) {
|
||||
const intl = useIntl();
|
||||
const currentMonday = useMemo(() => getCurrentMonday(), []);
|
||||
|
|
@ -115,11 +113,7 @@ export default function DailySection({
|
|||
|
||||
const [allRecords, setAllRecords] = useState<DailyEnergyData[]>([]);
|
||||
const [allHourlyRecords, setAllHourlyRecords] = useState<HourlyEnergyRecord[]>([]);
|
||||
const [selectedDate, setSelectedDate] = useState(() => {
|
||||
const date = formatDateISO(yesterday);
|
||||
onPeriodChange?.(date);
|
||||
return date;
|
||||
});
|
||||
const [selectedDate, setSelectedDate] = useState(formatDateISO(yesterday));
|
||||
const [selectedDayRecord, setSelectedDayRecord] = useState<DailyEnergyData | null>(null);
|
||||
const [hourlyRecords, setHourlyRecords] = useState<HourlyEnergyRecord[]>([]);
|
||||
const [loadingWeek, setLoadingWeek] = useState(false);
|
||||
|
|
@ -180,7 +174,6 @@ export default function DailySection({
|
|||
const handleStripSelect = (date: string) => {
|
||||
setSelectedDate(date);
|
||||
setNoData(false);
|
||||
onPeriodChange?.(date);
|
||||
};
|
||||
|
||||
const dt = new Date(selectedDate + 'T00:00:00');
|
||||
|
|
|
|||
|
|
@ -100,16 +100,14 @@ function SodistoreHomeConfiguration(props: SodistoreHomeConfigurationProps) {
|
|||
clusterNumber: props.values.Config.ClusterNumber ?? 1,
|
||||
PvNumber: props.values.Config.PvNumber ?? 0,
|
||||
timeChargeandDischargePower: props.values.Config?.TimeChargeandDischargePower ?? 0,
|
||||
startTimeChargeandDischargeDayandTime: (() => {
|
||||
const raw = props.values.Config?.StartTimeChargeandDischargeDayandTime;
|
||||
const parsed = raw ? dayjs(raw) : null;
|
||||
return parsed && parsed.year() >= 2020 ? parsed.toDate() : new Date();
|
||||
})(),
|
||||
stopTimeChargeandDischargeDayandTime: (() => {
|
||||
const raw = props.values.Config?.StopTimeChargeandDischargeDayandTime;
|
||||
const parsed = raw ? dayjs(raw) : null;
|
||||
return parsed && parsed.year() >= 2020 ? parsed.toDate() : new Date();
|
||||
})(),
|
||||
startTimeChargeandDischargeDayandTime:
|
||||
props.values.Config?.StartTimeChargeandDischargeDayandTime
|
||||
? dayjs(props.values.Config.StartTimeChargeandDischargeDayandTime).toDate()
|
||||
: null,
|
||||
stopTimeChargeandDischargeDayandTime:
|
||||
props.values.Config?.StopTimeChargeandDischargeDayandTime
|
||||
? dayjs(props.values.Config.StopTimeChargeandDischargeDayandTime).toDate()
|
||||
: null,
|
||||
controlPermission: String(props.values.Config.ControlPermission).toLowerCase() === "true",
|
||||
});
|
||||
|
||||
|
|
@ -202,11 +200,9 @@ function SodistoreHomeConfiguration(props: SodistoreHomeConfigurationProps) {
|
|||
});
|
||||
};
|
||||
|
||||
// Add time validation function — only relevant for Sinexcel BatteryPriority
|
||||
// Add time validation function
|
||||
const validateTimeOnly = () => {
|
||||
if (device === 4 &&
|
||||
OperatingPriorityOptions[formValues.operatingPriority] === 'BatteryPriority' &&
|
||||
formValues.startTimeChargeandDischargeDayandTime &&
|
||||
if (formValues.startTimeChargeandDischargeDayandTime &&
|
||||
formValues.stopTimeChargeandDischargeDayandTime) {
|
||||
const startHours = formValues.startTimeChargeandDischargeDayandTime.getHours();
|
||||
const startMinutes = formValues.startTimeChargeandDischargeDayandTime.getMinutes();
|
||||
|
|
|
|||
|
|
@ -236,8 +236,6 @@ function WeeklyReport({ installationId }: WeeklyReportProps) {
|
|||
const [regenerating, setRegenerating] = useState(false);
|
||||
const [dailyHasData, setDailyHasData] = useState(false);
|
||||
const [weeklyHasData, setWeeklyHasData] = useState(false);
|
||||
const [downloadingPdf, setDownloadingPdf] = useState(false);
|
||||
const [reportPeriod, setReportPeriod] = useState<{ start: string; end: string; year?: number; month?: number } | null>(null);
|
||||
const weeklyRef = useRef<WeeklySectionHandle>(null);
|
||||
|
||||
const fetchReportData = () => {
|
||||
|
|
@ -304,56 +302,16 @@ function WeeklyReport({ installationId }: WeeklyReportProps) {
|
|||
return false;
|
||||
})();
|
||||
|
||||
const handleDownloadPdf = async () => {
|
||||
const reportType = tabs[safeTab]?.key ?? 'report';
|
||||
let endpoint = '';
|
||||
const params: Record<string, any> = { installationId, language: intl.locale };
|
||||
|
||||
switch (reportType) {
|
||||
case 'daily':
|
||||
endpoint = '/GetDailyReportHtml';
|
||||
if (reportPeriod?.start) params.date = reportPeriod.start;
|
||||
break;
|
||||
case 'weekly':
|
||||
endpoint = '/GetWeeklyReportHtml';
|
||||
break;
|
||||
case 'monthly':
|
||||
endpoint = '/GetMonthlyReportHtml';
|
||||
if (reportPeriod?.year) params.year = reportPeriod.year;
|
||||
if (reportPeriod?.month) params.month = reportPeriod.month;
|
||||
break;
|
||||
case 'yearly':
|
||||
endpoint = '/GetYearlyReportHtml';
|
||||
if (reportPeriod?.year) params.year = reportPeriod.year;
|
||||
break;
|
||||
}
|
||||
|
||||
if (!endpoint) return;
|
||||
|
||||
setDownloadingPdf(true);
|
||||
try {
|
||||
const res = await axiosConfig.get(endpoint, { params, responseType: 'text' });
|
||||
const printWindow = window.open('', '_blank');
|
||||
if (!printWindow) return;
|
||||
|
||||
const dateRange = reportPeriod
|
||||
? `${reportPeriod.start.replace(/-/g, '')}-${reportPeriod.end.replace(/-/g, '')}`
|
||||
: new Date().toISOString().split('T')[0].replace(/-/g, '');
|
||||
|
||||
printWindow.document.write(res.data);
|
||||
printWindow.document.close();
|
||||
printWindow.document.title = `inesco-energy-${installationId}-${reportType}-${dateRange}`;
|
||||
printWindow.onafterprint = () => printWindow.close();
|
||||
setTimeout(() => printWindow.print(), 500);
|
||||
} catch (err) {
|
||||
console.error('PDF download failed', err);
|
||||
} finally {
|
||||
setDownloadingPdf(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Box sx={{ p: 2, width: '100%', maxWidth: 900, mx: 'auto' }} className="report-container">
|
||||
<style>{`
|
||||
@media print {
|
||||
body * { visibility: hidden; }
|
||||
.report-container, .report-container * { visibility: visible; }
|
||||
.report-container { position: absolute; left: 0; top: 0; width: 100%; padding: 20px; }
|
||||
.no-print { display: none !important; }
|
||||
}
|
||||
`}</style>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', mb: 2 }} className="no-print">
|
||||
<Tabs
|
||||
value={safeTab}
|
||||
|
|
@ -365,9 +323,8 @@ function WeeklyReport({ installationId }: WeeklyReportProps) {
|
|||
{activeTabHasData && (
|
||||
<Button
|
||||
variant="outlined"
|
||||
startIcon={downloadingPdf ? <CircularProgress size={16} /> : <DownloadIcon />}
|
||||
onClick={handleDownloadPdf}
|
||||
disabled={downloadingPdf}
|
||||
startIcon={<DownloadIcon />}
|
||||
onClick={() => window.print()}
|
||||
sx={{ ml: 2, whiteSpace: 'nowrap' }}
|
||||
>
|
||||
<FormattedMessage id="downloadPdf" defaultMessage="Download PDF" />
|
||||
|
|
@ -404,7 +361,7 @@ function WeeklyReport({ installationId }: WeeklyReportProps) {
|
|||
</Box>
|
||||
|
||||
<Box sx={{ display: tabs[safeTab]?.key === 'daily' ? 'block' : 'none', minHeight: '50vh' }}>
|
||||
<DailySection installationId={installationId} onHasData={setDailyHasData} onPeriodChange={(date: string) => setReportPeriod({ start: date, end: date })} />
|
||||
<DailySection installationId={installationId} onHasData={setDailyHasData} />
|
||||
</Box>
|
||||
<Box sx={{ display: tabs[safeTab]?.key === 'weekly' ? 'block' : 'none', minHeight: '50vh' }}>
|
||||
<WeeklySection
|
||||
|
|
@ -416,7 +373,6 @@ function WeeklyReport({ installationId }: WeeklyReportProps) {
|
|||
: null
|
||||
}
|
||||
onHasData={setWeeklyHasData}
|
||||
onPeriodChange={(start, end) => setReportPeriod({ start, end })}
|
||||
/>
|
||||
</Box>
|
||||
<Box sx={{ display: tabs[safeTab]?.key === 'monthly' ? 'block' : 'none', minHeight: '50vh' }}>
|
||||
|
|
@ -428,7 +384,6 @@ function WeeklyReport({ installationId }: WeeklyReportProps) {
|
|||
onGenerate={handleGenerateMonthly}
|
||||
selectedIdx={selectedMonthlyIdx}
|
||||
onSelectedIdxChange={setSelectedMonthlyIdx}
|
||||
onPeriodChange={(r: MonthlyReport) => setReportPeriod({ start: r.periodStart, end: r.periodEnd, year: r.year, month: r.month })}
|
||||
/>
|
||||
</Box>
|
||||
<Box sx={{ display: tabs[safeTab]?.key === 'yearly' ? 'block' : 'none', minHeight: '50vh' }}>
|
||||
|
|
@ -440,7 +395,6 @@ function WeeklyReport({ installationId }: WeeklyReportProps) {
|
|||
onGenerate={handleGenerateYearly}
|
||||
selectedIdx={selectedYearlyIdx}
|
||||
onSelectedIdxChange={setSelectedYearlyIdx}
|
||||
onPeriodChange={(r: YearlyReport) => setReportPeriod({ start: r.periodStart, end: r.periodEnd, year: r.year })}
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
|
|
@ -453,8 +407,8 @@ interface WeeklySectionHandle {
|
|||
regenerate: () => void;
|
||||
}
|
||||
|
||||
const WeeklySection = forwardRef<WeeklySectionHandle, { installationId: number; latestMonthlyPeriodEnd: string | null; onHasData?: (hasData: boolean) => void; onPeriodChange?: (start: string, end: string) => void }>(
|
||||
({ installationId, latestMonthlyPeriodEnd, onHasData, onPeriodChange }, ref) => {
|
||||
const WeeklySection = forwardRef<WeeklySectionHandle, { installationId: number; latestMonthlyPeriodEnd: string | null; onHasData?: (hasData: boolean) => void }>(
|
||||
({ installationId, latestMonthlyPeriodEnd, onHasData }, ref) => {
|
||||
const intl = useIntl();
|
||||
const [report, setReport] = useState<WeeklyReportResponse | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
|
@ -473,7 +427,6 @@ const WeeklySection = forwardRef<WeeklySectionHandle, { installationId: number;
|
|||
});
|
||||
setReport(res.data);
|
||||
onHasData?.(true);
|
||||
onPeriodChange?.(res.data.periodStart, res.data.periodEnd);
|
||||
} catch (err: any) {
|
||||
const msg =
|
||||
err.response?.data ||
|
||||
|
|
@ -858,8 +811,7 @@ function MonthlySection({
|
|||
generating,
|
||||
onGenerate,
|
||||
selectedIdx,
|
||||
onSelectedIdxChange,
|
||||
onPeriodChange
|
||||
onSelectedIdxChange
|
||||
}: {
|
||||
installationId: number;
|
||||
reports: MonthlyReport[];
|
||||
|
|
@ -868,7 +820,6 @@ function MonthlySection({
|
|||
onGenerate: (year: number, month: number) => void;
|
||||
selectedIdx: number;
|
||||
onSelectedIdxChange: (idx: number) => void;
|
||||
onPeriodChange?: (report: MonthlyReport) => void;
|
||||
}) {
|
||||
const intl = useIntl();
|
||||
|
||||
|
|
@ -920,7 +871,6 @@ function MonthlySection({
|
|||
sendParamsFn={(r: MonthlyReport) => ({ installationId, year: r.year, month: r.month })}
|
||||
controlledIdx={selectedIdx}
|
||||
onIdxChange={onSelectedIdxChange}
|
||||
onPeriodChange={onPeriodChange}
|
||||
/>
|
||||
) : pendingMonths.length === 0 ? (
|
||||
<Box sx={{ display: 'flex', justifyContent: 'center', alignItems: 'center', py: 6 }}>
|
||||
|
|
@ -942,8 +892,7 @@ function YearlySection({
|
|||
generating,
|
||||
onGenerate,
|
||||
selectedIdx,
|
||||
onSelectedIdxChange,
|
||||
onPeriodChange
|
||||
onSelectedIdxChange
|
||||
}: {
|
||||
installationId: number;
|
||||
reports: YearlyReport[];
|
||||
|
|
@ -952,7 +901,6 @@ function YearlySection({
|
|||
onGenerate: (year: number) => void;
|
||||
selectedIdx: number;
|
||||
onSelectedIdxChange: (idx: number) => void;
|
||||
onPeriodChange?: (report: YearlyReport) => void;
|
||||
}) {
|
||||
const intl = useIntl();
|
||||
|
||||
|
|
@ -1004,7 +952,6 @@ function YearlySection({
|
|||
sendParamsFn={(r: YearlyReport) => ({ installationId, year: r.year })}
|
||||
controlledIdx={selectedIdx}
|
||||
onIdxChange={onSelectedIdxChange}
|
||||
onPeriodChange={onPeriodChange}
|
||||
/>
|
||||
) : pendingYears.length === 0 ? (
|
||||
<Box sx={{ display: 'flex', justifyContent: 'center', alignItems: 'center', py: 6 }}>
|
||||
|
|
@ -1028,8 +975,7 @@ function AggregatedSection<T extends ReportSummary>({
|
|||
sendEndpoint,
|
||||
sendParamsFn,
|
||||
controlledIdx,
|
||||
onIdxChange,
|
||||
onPeriodChange
|
||||
onIdxChange
|
||||
}: {
|
||||
reports: T[];
|
||||
type: 'monthly' | 'yearly';
|
||||
|
|
@ -1040,7 +986,6 @@ function AggregatedSection<T extends ReportSummary>({
|
|||
sendParamsFn: (r: T) => object;
|
||||
controlledIdx?: number;
|
||||
onIdxChange?: (idx: number) => void;
|
||||
onPeriodChange?: (report: T) => void;
|
||||
}) {
|
||||
const intl = useIntl();
|
||||
const [internalIdx, setInternalIdx] = useState(0);
|
||||
|
|
@ -1048,16 +993,8 @@ function AggregatedSection<T extends ReportSummary>({
|
|||
const handleIdxChange = (idx: number) => {
|
||||
setInternalIdx(idx);
|
||||
onIdxChange?.(idx);
|
||||
if (reports[idx]) onPeriodChange?.(reports[idx]);
|
||||
};
|
||||
|
||||
// Report initial period on mount
|
||||
useEffect(() => {
|
||||
if (reports.length > 0 && reports[selectedIdx]) {
|
||||
onPeriodChange?.(reports[selectedIdx]);
|
||||
}
|
||||
}, [reports.length]);
|
||||
|
||||
if (reports.length === 0) {
|
||||
return (
|
||||
<Box sx={{ display: 'flex', justifyContent: 'center', alignItems: 'center', py: 6 }}>
|
||||
|
|
|
|||
Loading…
Reference in New Issue