cahched weekly report
This commit is contained in:
parent
7f972d13c3
commit
f7ee347fc5
|
|
@ -922,12 +922,13 @@ public class Controller : ControllerBase
|
||||||
// ── Weekly Performance Report ──────────────────────────────────────
|
// ── Weekly Performance Report ──────────────────────────────────────
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Generates a weekly performance report from an Excel file in tmp_report/{installationId}.xlsx
|
/// Returns a weekly performance report. Serves from cache if available;
|
||||||
/// Returns JSON with daily data, weekly totals, ratios, and AI insight.
|
/// generates fresh on first request or when forceRegenerate is true.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[HttpGet(nameof(GetWeeklyReport))]
|
[HttpGet(nameof(GetWeeklyReport))]
|
||||||
public async Task<ActionResult<WeeklyReportResponse>> GetWeeklyReport(
|
public async Task<ActionResult<WeeklyReportResponse>> GetWeeklyReport(
|
||||||
Int64 installationId, Token authToken, String? language = null, String? weekStart = null)
|
Int64 installationId, Token authToken, String? language = null,
|
||||||
|
String? weekStart = null, Boolean forceRegenerate = false)
|
||||||
{
|
{
|
||||||
var user = Db.GetSession(authToken)?.User;
|
var user = Db.GetSession(authToken)?.User;
|
||||||
if (user == null)
|
if (user == null)
|
||||||
|
|
@ -949,6 +950,39 @@ public class Controller : ControllerBase
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var lang = language ?? user.Language ?? "en";
|
var lang = language ?? user.Language ?? "en";
|
||||||
|
|
||||||
|
// Compute target week dates for cache lookup
|
||||||
|
DateOnly periodStart, periodEnd;
|
||||||
|
if (weekStartDate.HasValue)
|
||||||
|
{
|
||||||
|
periodStart = weekStartDate.Value;
|
||||||
|
periodEnd = weekStartDate.Value.AddDays(6);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
(periodStart, periodEnd) = WeeklyReportService.LastCalendarWeek();
|
||||||
|
}
|
||||||
|
|
||||||
|
var periodStartStr = periodStart.ToString("yyyy-MM-dd");
|
||||||
|
var periodEndStr = periodEnd.ToString("yyyy-MM-dd");
|
||||||
|
|
||||||
|
// Cache-first: check if a cached report exists for this week
|
||||||
|
if (!forceRegenerate)
|
||||||
|
{
|
||||||
|
var cached = Db.GetWeeklyReportForWeek(installationId, periodStartStr, periodEndStr);
|
||||||
|
if (cached != null)
|
||||||
|
{
|
||||||
|
var cachedResponse = await ReportAggregationService.ToWeeklyReportResponseAsync(cached, lang);
|
||||||
|
if (cachedResponse != null)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"[GetWeeklyReport] Serving cached report for installation {installationId}, period {periodStartStr}–{periodEndStr}, language={lang}");
|
||||||
|
return Ok(cachedResponse);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cache miss or forceRegenerate: generate fresh
|
||||||
|
Console.WriteLine($"[GetWeeklyReport] Generating fresh report for installation {installationId}, period {periodStartStr}–{periodEndStr}");
|
||||||
var report = await WeeklyReportService.GenerateReportAsync(
|
var report = await WeeklyReportService.GenerateReportAsync(
|
||||||
installationId, installation.InstallationName, lang, weekStartDate);
|
installationId, installation.InstallationName, lang, weekStartDate);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -43,6 +43,12 @@ public class WeeklyReportSummary
|
||||||
// AI insight for this week
|
// AI insight for this week
|
||||||
public String AiInsight { get; set; } = "";
|
public String AiInsight { get; set; } = "";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Full serialized WeeklyReportResponse (with AiInsight cleared).
|
||||||
|
/// Used for cache-first serving — avoids regenerating numeric data + Mistral call.
|
||||||
|
/// </summary>
|
||||||
|
public String ResponseJson { get; set; } = "";
|
||||||
|
|
||||||
public String CreatedAt { get; set; } = "";
|
public String CreatedAt { get; set; } = "";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -77,6 +77,18 @@ public static partial class Db
|
||||||
.ToList();
|
.ToList();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Finds a cached weekly report whose period overlaps with the given date range.
|
||||||
|
/// Uses overlap logic (not exact match) because PeriodStart may be offset
|
||||||
|
/// if the first day of the week has no data.
|
||||||
|
/// </summary>
|
||||||
|
public static WeeklyReportSummary? GetWeeklyReportForWeek(Int64 installationId, String periodStart, String periodEnd)
|
||||||
|
=> WeeklyReports
|
||||||
|
.Where(r => r.InstallationId == installationId)
|
||||||
|
.ToList()
|
||||||
|
.FirstOrDefault(r => String.Compare(r.PeriodStart, periodEnd, StringComparison.Ordinal) <= 0
|
||||||
|
&& String.Compare(r.PeriodEnd, periodStart, StringComparison.Ordinal) >= 0);
|
||||||
|
|
||||||
public static List<MonthlyReportSummary> GetMonthlyReports(Int64 installationId)
|
public static List<MonthlyReportSummary> GetMonthlyReports(Int64 installationId)
|
||||||
=> MonthlyReports
|
=> MonthlyReports
|
||||||
.Where(r => r.InstallationId == installationId)
|
.Where(r => r.InstallationId == installationId)
|
||||||
|
|
|
||||||
|
|
@ -185,6 +185,29 @@ public static class ReportAggregationService
|
||||||
foreach (var old in overlapping)
|
foreach (var old in overlapping)
|
||||||
Db.WeeklyReports.Delete(r => r.Id == old.Id);
|
Db.WeeklyReports.Delete(r => r.Id == old.Id);
|
||||||
|
|
||||||
|
// Serialize full response (minus AI insight) for cache-first serving
|
||||||
|
var reportForCache = new WeeklyReportResponse
|
||||||
|
{
|
||||||
|
InstallationName = report.InstallationName,
|
||||||
|
PeriodStart = report.PeriodStart,
|
||||||
|
PeriodEnd = report.PeriodEnd,
|
||||||
|
CurrentWeek = report.CurrentWeek,
|
||||||
|
PreviousWeek = report.PreviousWeek,
|
||||||
|
TotalEnergySaved = report.TotalEnergySaved,
|
||||||
|
TotalSavingsCHF = report.TotalSavingsCHF,
|
||||||
|
DaysEquivalent = report.DaysEquivalent,
|
||||||
|
SelfSufficiencyPercent = report.SelfSufficiencyPercent,
|
||||||
|
SelfConsumptionPercent = report.SelfConsumptionPercent,
|
||||||
|
BatteryEfficiencyPercent = report.BatteryEfficiencyPercent,
|
||||||
|
GridDependencyPercent = report.GridDependencyPercent,
|
||||||
|
PvChangePercent = report.PvChangePercent,
|
||||||
|
ConsumptionChangePercent = report.ConsumptionChangePercent,
|
||||||
|
GridImportChangePercent = report.GridImportChangePercent,
|
||||||
|
DailyData = report.DailyData,
|
||||||
|
Behavior = report.Behavior,
|
||||||
|
AiInsight = "", // Language-dependent; stored in AiInsightCache
|
||||||
|
};
|
||||||
|
|
||||||
var summary = new WeeklyReportSummary
|
var summary = new WeeklyReportSummary
|
||||||
{
|
{
|
||||||
InstallationId = installationId,
|
InstallationId = installationId,
|
||||||
|
|
@ -207,6 +230,7 @@ public static class ReportAggregationService
|
||||||
WeekdayAvgDailyLoad = report.Behavior?.WeekdayAvgDailyLoad ?? 0,
|
WeekdayAvgDailyLoad = report.Behavior?.WeekdayAvgDailyLoad ?? 0,
|
||||||
WeekendAvgDailyLoad = report.Behavior?.WeekendAvgDailyLoad ?? 0,
|
WeekendAvgDailyLoad = report.Behavior?.WeekendAvgDailyLoad ?? 0,
|
||||||
AiInsight = report.AiInsight,
|
AiInsight = report.AiInsight,
|
||||||
|
ResponseJson = JsonConvert.SerializeObject(reportForCache),
|
||||||
CreatedAt = DateTime.UtcNow.ToString("o"),
|
CreatedAt = DateTime.UtcNow.ToString("o"),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -574,6 +598,25 @@ public static class ReportAggregationService
|
||||||
() => GenerateWeeklySummaryAiInsightAsync(report, installationName, language));
|
() => GenerateWeeklySummaryAiInsightAsync(report, installationName, language));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Reconstructs a full WeeklyReportResponse from a cached WeeklyReportSummary.
|
||||||
|
/// Returns null if ResponseJson is empty (old records without cache data).
|
||||||
|
/// AI insight is fetched/generated per-language via AiInsightCache.
|
||||||
|
/// </summary>
|
||||||
|
public static async Task<WeeklyReportResponse?> ToWeeklyReportResponseAsync(
|
||||||
|
WeeklyReportSummary summary, String language)
|
||||||
|
{
|
||||||
|
if (String.IsNullOrEmpty(summary.ResponseJson))
|
||||||
|
return null;
|
||||||
|
|
||||||
|
var response = JsonConvert.DeserializeObject<WeeklyReportResponse>(summary.ResponseJson);
|
||||||
|
if (response == null)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
response.AiInsight = await GetOrGenerateWeeklyInsightAsync(summary, language);
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>Cached-or-generated AI insight for a stored MonthlyReportSummary.</summary>
|
/// <summary>Cached-or-generated AI insight for a stored MonthlyReportSummary.</summary>
|
||||||
public static Task<String> GetOrGenerateMonthlyInsightAsync(
|
public static Task<String> GetOrGenerateMonthlyInsightAsync(
|
||||||
MonthlyReportSummary report, String language)
|
MonthlyReportSummary report, String language)
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,7 @@ import {
|
||||||
} from '@mui/material';
|
} from '@mui/material';
|
||||||
import SendIcon from '@mui/icons-material/Send';
|
import SendIcon from '@mui/icons-material/Send';
|
||||||
import DownloadIcon from '@mui/icons-material/Download';
|
import DownloadIcon from '@mui/icons-material/Download';
|
||||||
|
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';
|
||||||
|
|
||||||
|
|
@ -514,12 +515,12 @@ function WeeklySection({ installationId, latestMonthlyPeriodEnd }: { installatio
|
||||||
fetchReport();
|
fetchReport();
|
||||||
}, [installationId, intl.locale]);
|
}, [installationId, intl.locale]);
|
||||||
|
|
||||||
const fetchReport = async () => {
|
const fetchReport = async (forceRegenerate = false) => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
setError(null);
|
setError(null);
|
||||||
try {
|
try {
|
||||||
const res = await axiosConfig.get('/GetWeeklyReport', {
|
const res = await axiosConfig.get('/GetWeeklyReport', {
|
||||||
params: { installationId, language: intl.locale }
|
params: { installationId, language: intl.locale, forceRegenerate }
|
||||||
});
|
});
|
||||||
setReport(res.data);
|
setReport(res.data);
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
|
|
@ -614,6 +615,8 @@ 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>
|
||||||
|
|
@ -623,6 +626,17 @@ 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>
|
||||||
|
|
||||||
{/* Weekly Insights */}
|
{/* Weekly Insights */}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue