disable download and email service when there is no report data

This commit is contained in:
Yinyin Liu 2026-03-12 14:56:26 +01:00
parent 50bc85ff2a
commit d54fc1c2ab
2 changed files with 112 additions and 60 deletions

View File

@ -96,9 +96,11 @@ function getCurrentWeekDays(currentMonday: Date): Date[] {
// ── Main Component ─────────────────────────────────────────── // ── Main Component ───────────────────────────────────────────
export default function DailySection({ export default function DailySection({
installationId installationId,
onHasData
}: { }: {
installationId: number; installationId: number;
onHasData?: (hasData: boolean) => void;
}) { }) {
const intl = useIntl(); const intl = useIntl();
const currentMonday = useMemo(() => getCurrentMonday(), []); const currentMonday = useMemo(() => getCurrentMonday(), []);
@ -138,10 +140,12 @@ export default function DailySection({
const hourly = res.data?.hourlyRecords?.records ?? []; const hourly = res.data?.hourlyRecords?.records ?? [];
setAllRecords(Array.isArray(daily) ? daily : []); setAllRecords(Array.isArray(daily) ? daily : []);
setAllHourlyRecords(Array.isArray(hourly) ? hourly : []); setAllHourlyRecords(Array.isArray(hourly) ? hourly : []);
onHasData?.(Array.isArray(daily) && daily.length > 0);
}) })
.catch(() => { .catch(() => {
setAllRecords([]); setAllRecords([]);
setAllHourlyRecords([]); setAllHourlyRecords([]);
onHasData?.(false);
}) })
.finally(() => setLoadingWeek(false)); .finally(() => setLoadingWeek(false));
}, [installationId, weekDays]); }, [installationId, weekDays]);

View File

@ -1,4 +1,4 @@
import { useEffect, useState } from 'react'; import { useEffect, useImperativeHandle, useRef, useState, forwardRef } from 'react';
import { useIntl, FormattedMessage } from 'react-intl'; import { useIntl, FormattedMessage } from 'react-intl';
import { import {
Accordion, Accordion,
@ -231,6 +231,12 @@ function WeeklyReport({ installationId }: WeeklyReportProps) {
const [pendingMonths, setPendingMonths] = useState<PendingMonth[]>([]); const [pendingMonths, setPendingMonths] = useState<PendingMonth[]>([]);
const [pendingYears, setPendingYears] = useState<PendingYear[]>([]); const [pendingYears, setPendingYears] = useState<PendingYear[]>([]);
const [generating, setGenerating] = useState<string | null>(null); const [generating, setGenerating] = useState<string | null>(null);
const [selectedMonthlyIdx, setSelectedMonthlyIdx] = useState(0);
const [selectedYearlyIdx, setSelectedYearlyIdx] = useState(0);
const [regenerating, setRegenerating] = useState(false);
const [dailyHasData, setDailyHasData] = useState(false);
const [weeklyHasData, setWeeklyHasData] = useState(false);
const weeklyRef = useRef<WeeklySectionHandle>(null);
const fetchReportData = () => { const fetchReportData = () => {
const lang = intl.locale; const lang = intl.locale;
@ -287,6 +293,15 @@ function WeeklyReport({ installationId }: WeeklyReportProps) {
const safeTab = Math.min(activeTab, tabs.length - 1); const safeTab = Math.min(activeTab, tabs.length - 1);
const activeTabHasData = (() => {
const key = tabs[safeTab]?.key;
if (key === 'daily') return dailyHasData;
if (key === 'weekly') return weeklyHasData;
if (key === 'monthly') return monthlyReports.length > 0;
if (key === 'yearly') return yearlyReports.length > 0;
return false;
})();
return ( return (
<Box sx={{ p: 2, width: '100%', maxWidth: 900, mx: 'auto' }} className="report-container"> <Box sx={{ p: 2, width: '100%', maxWidth: 900, mx: 'auto' }} className="report-container">
<style>{` <style>{`
@ -305,6 +320,7 @@ function WeeklyReport({ installationId }: WeeklyReportProps) {
> >
{tabs.map(t => <Tab key={t.key} label={t.label} />)} {tabs.map(t => <Tab key={t.key} label={t.label} />)}
</Tabs> </Tabs>
{activeTabHasData && (
<Button <Button
variant="outlined" variant="outlined"
startIcon={<DownloadIcon />} startIcon={<DownloadIcon />}
@ -313,19 +329,50 @@ function WeeklyReport({ installationId }: WeeklyReportProps) {
> >
<FormattedMessage id="downloadPdf" defaultMessage="Download PDF" /> <FormattedMessage id="downloadPdf" defaultMessage="Download PDF" />
</Button> </Button>
)}
{tabs[safeTab]?.key !== 'daily' && activeTabHasData && (
<Button
variant="outlined"
disabled={regenerating || generating !== null}
startIcon={(regenerating || generating !== null) ? <CircularProgress size={16} /> : <RefreshIcon />}
onClick={async () => {
const key = tabs[safeTab]?.key;
if (key === 'weekly') {
weeklyRef.current?.regenerate();
} else if (key === 'monthly') {
const r = monthlyReports[selectedMonthlyIdx];
if (r) {
setRegenerating(true);
try { await handleGenerateMonthly(r.year, r.month); } finally { setRegenerating(false); }
}
} else if (key === 'yearly') {
const r = yearlyReports[selectedYearlyIdx];
if (r) {
setRegenerating(true);
try { await handleGenerateYearly(r.year); } finally { setRegenerating(false); }
}
}
}}
sx={{ ml: 1, whiteSpace: 'nowrap' }}
>
<FormattedMessage id="regenerateReport" defaultMessage="Regenerate" />
</Button>
)}
</Box> </Box>
<Box sx={{ display: tabs[safeTab]?.key === 'daily' ? 'block' : 'none', minHeight: '50vh' }}> <Box sx={{ display: tabs[safeTab]?.key === 'daily' ? 'block' : 'none', minHeight: '50vh' }}>
<DailySection installationId={installationId} /> <DailySection installationId={installationId} onHasData={setDailyHasData} />
</Box> </Box>
<Box sx={{ display: tabs[safeTab]?.key === 'weekly' ? 'block' : 'none', minHeight: '50vh' }}> <Box sx={{ display: tabs[safeTab]?.key === 'weekly' ? 'block' : 'none', minHeight: '50vh' }}>
<WeeklySection <WeeklySection
ref={weeklyRef}
installationId={installationId} installationId={installationId}
latestMonthlyPeriodEnd={ latestMonthlyPeriodEnd={
monthlyReports.length > 0 monthlyReports.length > 0
? monthlyReports.reduce((a, b) => a.periodEnd > b.periodEnd ? a : b).periodEnd ? monthlyReports.reduce((a, b) => a.periodEnd > b.periodEnd ? a : b).periodEnd
: null : null
} }
onHasData={setWeeklyHasData}
/> />
</Box> </Box>
<Box sx={{ display: tabs[safeTab]?.key === 'monthly' ? 'block' : 'none', minHeight: '50vh' }}> <Box sx={{ display: tabs[safeTab]?.key === 'monthly' ? 'block' : 'none', minHeight: '50vh' }}>
@ -335,6 +382,8 @@ function WeeklyReport({ installationId }: WeeklyReportProps) {
pendingMonths={pendingMonths} pendingMonths={pendingMonths}
generating={generating} generating={generating}
onGenerate={handleGenerateMonthly} onGenerate={handleGenerateMonthly}
selectedIdx={selectedMonthlyIdx}
onSelectedIdxChange={setSelectedMonthlyIdx}
/> />
</Box> </Box>
<Box sx={{ display: tabs[safeTab]?.key === 'yearly' ? 'block' : 'none', minHeight: '50vh' }}> <Box sx={{ display: tabs[safeTab]?.key === 'yearly' ? 'block' : 'none', minHeight: '50vh' }}>
@ -344,6 +393,8 @@ function WeeklyReport({ installationId }: WeeklyReportProps) {
pendingYears={pendingYears} pendingYears={pendingYears}
generating={generating} generating={generating}
onGenerate={handleGenerateYearly} onGenerate={handleGenerateYearly}
selectedIdx={selectedYearlyIdx}
onSelectedIdxChange={setSelectedYearlyIdx}
/> />
</Box> </Box>
</Box> </Box>
@ -352,7 +403,12 @@ function WeeklyReport({ installationId }: WeeklyReportProps) {
// ── Weekly Section (existing weekly report content) ──────────── // ── Weekly Section (existing weekly report content) ────────────
function WeeklySection({ installationId, latestMonthlyPeriodEnd }: { installationId: number; latestMonthlyPeriodEnd: string | null }) { interface WeeklySectionHandle {
regenerate: () => void;
}
const WeeklySection = forwardRef<WeeklySectionHandle, { installationId: number; latestMonthlyPeriodEnd: string | null; onHasData?: (hasData: boolean) => void }>(
({ installationId, latestMonthlyPeriodEnd, onHasData }, ref) => {
const intl = useIntl(); const intl = useIntl();
const [report, setReport] = useState<WeeklyReportResponse | null>(null); const [report, setReport] = useState<WeeklyReportResponse | null>(null);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
@ -370,17 +426,21 @@ function WeeklySection({ installationId, latestMonthlyPeriodEnd }: { installatio
params: { installationId, language: intl.locale, forceRegenerate } params: { installationId, language: intl.locale, forceRegenerate }
}); });
setReport(res.data); setReport(res.data);
onHasData?.(true);
} catch (err: any) { } catch (err: any) {
const msg = const msg =
err.response?.data || err.response?.data ||
err.message || err.message ||
intl.formatMessage({ id: 'failedToLoadReport' }); intl.formatMessage({ id: 'failedToLoadReport' });
setError(typeof msg === 'string' ? msg : JSON.stringify(msg)); setError(typeof msg === 'string' ? msg : JSON.stringify(msg));
onHasData?.(false);
} finally { } finally {
setLoading(false); setLoading(false);
} }
}; };
useImperativeHandle(ref, () => ({ regenerate: () => fetchReport(true) }));
const handleSendEmail = async (emailAddress: string) => { const handleSendEmail = async (emailAddress: string) => {
await axiosConfig.post('/SendWeeklyReportEmail', null, { await axiosConfig.post('/SendWeeklyReportEmail', null, {
params: { installationId, emailAddress } params: { installationId, emailAddress }
@ -464,8 +524,6 @@ function WeeklySection({ installationId, latestMonthlyPeriodEnd }: { installatio
borderRadius: 2 borderRadius: 2
}} }}
> >
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start' }}>
<Box>
<Typography variant="h5" fontWeight="bold"> <Typography variant="h5" fontWeight="bold">
<FormattedMessage id="reportTitle" defaultMessage="Weekly Performance Report" /> <FormattedMessage id="reportTitle" defaultMessage="Weekly Performance Report" />
</Typography> </Typography>
@ -475,17 +533,6 @@ function WeeklySection({ installationId, latestMonthlyPeriodEnd }: { installatio
<Typography variant="body2" sx={{ opacity: 0.7 }}> <Typography variant="body2" sx={{ opacity: 0.7 }}>
{report.periodStart} {report.periodEnd} {report.periodStart} {report.periodEnd}
</Typography> </Typography>
</Box>
<Button
variant="outlined"
size="small"
startIcon={<RefreshIcon />}
onClick={() => fetchReport(true)}
sx={{ color: '#fff', borderColor: 'rgba(255,255,255,0.5)', '&:hover': { borderColor: '#fff' } }}
>
<FormattedMessage id="regenerateReport" defaultMessage="Regenerate" />
</Button>
</Box>
</Paper> </Paper>
{/* Missing days warning */} {/* Missing days warning */}
@ -638,7 +685,7 @@ function WeeklySection({ installationId, latestMonthlyPeriodEnd }: { installatio
/> />
</> </>
); );
} });
// ── Weekly History (saved weekly reports for current month) ───── // ── Weekly History (saved weekly reports for current month) ─────
@ -762,13 +809,17 @@ function MonthlySection({
reports, reports,
pendingMonths, pendingMonths,
generating, generating,
onGenerate onGenerate,
selectedIdx,
onSelectedIdxChange
}: { }: {
installationId: number; installationId: number;
reports: MonthlyReport[]; reports: MonthlyReport[];
pendingMonths: PendingMonth[]; pendingMonths: PendingMonth[];
generating: string | null; generating: string | null;
onGenerate: (year: number, month: number) => void; onGenerate: (year: number, month: number) => void;
selectedIdx: number;
onSelectedIdxChange: (idx: number) => void;
}) { }) {
const intl = useIntl(); const intl = useIntl();
@ -818,7 +869,8 @@ function MonthlySection({
countFn={(r: MonthlyReport) => r.weekCount} countFn={(r: MonthlyReport) => r.weekCount}
sendEndpoint="/SendMonthlyReportEmail" sendEndpoint="/SendMonthlyReportEmail"
sendParamsFn={(r: MonthlyReport) => ({ installationId, year: r.year, month: r.month })} sendParamsFn={(r: MonthlyReport) => ({ installationId, year: r.year, month: r.month })}
onRegenerate={(r: MonthlyReport) => onGenerate(r.year, r.month)} controlledIdx={selectedIdx}
onIdxChange={onSelectedIdxChange}
/> />
) : pendingMonths.length === 0 ? ( ) : pendingMonths.length === 0 ? (
<Box sx={{ display: 'flex', justifyContent: 'center', alignItems: 'center', py: 6 }}> <Box sx={{ display: 'flex', justifyContent: 'center', alignItems: 'center', py: 6 }}>
@ -838,13 +890,17 @@ function YearlySection({
reports, reports,
pendingYears, pendingYears,
generating, generating,
onGenerate onGenerate,
selectedIdx,
onSelectedIdxChange
}: { }: {
installationId: number; installationId: number;
reports: YearlyReport[]; reports: YearlyReport[];
pendingYears: PendingYear[]; pendingYears: PendingYear[];
generating: string | null; generating: string | null;
onGenerate: (year: number) => void; onGenerate: (year: number) => void;
selectedIdx: number;
onSelectedIdxChange: (idx: number) => void;
}) { }) {
const intl = useIntl(); const intl = useIntl();
@ -894,7 +950,8 @@ function YearlySection({
countFn={(r: YearlyReport) => r.monthCount} countFn={(r: YearlyReport) => r.monthCount}
sendEndpoint="/SendYearlyReportEmail" sendEndpoint="/SendYearlyReportEmail"
sendParamsFn={(r: YearlyReport) => ({ installationId, year: r.year })} sendParamsFn={(r: YearlyReport) => ({ installationId, year: r.year })}
onRegenerate={(r: YearlyReport) => onGenerate(r.year)} controlledIdx={selectedIdx}
onIdxChange={onSelectedIdxChange}
/> />
) : pendingYears.length === 0 ? ( ) : pendingYears.length === 0 ? (
<Box sx={{ display: 'flex', justifyContent: 'center', alignItems: 'center', py: 6 }}> <Box sx={{ display: 'flex', justifyContent: 'center', alignItems: 'center', py: 6 }}>
@ -917,7 +974,8 @@ function AggregatedSection<T extends ReportSummary>({
countFn, countFn,
sendEndpoint, sendEndpoint,
sendParamsFn, sendParamsFn,
onRegenerate controlledIdx,
onIdxChange
}: { }: {
reports: T[]; reports: T[];
type: 'monthly' | 'yearly'; type: 'monthly' | 'yearly';
@ -926,11 +984,16 @@ function AggregatedSection<T extends ReportSummary>({
countFn: (r: T) => number; countFn: (r: T) => number;
sendEndpoint: string; sendEndpoint: string;
sendParamsFn: (r: T) => object; sendParamsFn: (r: T) => object;
onRegenerate?: (r: T) => void | Promise<void>; controlledIdx?: number;
onIdxChange?: (idx: number) => void;
}) { }) {
const intl = useIntl(); const intl = useIntl();
const [selectedIdx, setSelectedIdx] = useState(0); const [internalIdx, setInternalIdx] = useState(0);
const [regenerating, setRegenerating] = useState(false); const selectedIdx = controlledIdx ?? internalIdx;
const handleIdxChange = (idx: number) => {
setInternalIdx(idx);
onIdxChange?.(idx);
};
if (reports.length === 0) { if (reports.length === 0) {
return ( return (
@ -966,7 +1029,7 @@ function AggregatedSection<T extends ReportSummary>({
{reports.length > 1 && ( {reports.length > 1 && (
<Select <Select
value={selectedIdx} value={selectedIdx}
onChange={(e) => setSelectedIdx(Number(e.target.value))} onChange={(e) => handleIdxChange(Number(e.target.value))}
size="small" size="small"
sx={{ minWidth: 200 }} sx={{ minWidth: 200 }}
> >
@ -975,22 +1038,7 @@ function AggregatedSection<T extends ReportSummary>({
))} ))}
</Select> </Select>
)} )}
<Box sx={{ ml: 'auto', display: 'flex', alignItems: 'center', gap: 1 }}> <Box sx={{ ml: 'auto' }}>
{onRegenerate && (
<Button
variant="outlined"
size="small"
disabled={regenerating}
startIcon={regenerating ? <CircularProgress size={14} /> : <RefreshIcon />}
onClick={async () => {
setRegenerating(true);
try { await onRegenerate(r); } finally { setRegenerating(false); }
}}
sx={{ textTransform: 'none' }}
>
<FormattedMessage id="regenerateReport" defaultMessage="Regenerate" />
</Button>
)}
<EmailBar onSend={handleSendEmail} /> <EmailBar onSend={handleSendEmail} />
</Box> </Box>
</Box> </Box>