cahched weekly report
This commit is contained in:
parent
7f972d13c3
commit
f7ee347fc5
|
|
@ -922,12 +922,13 @@ public class Controller : ControllerBase
|
|||
// ── Weekly Performance Report ──────────────────────────────────────
|
||||
|
||||
/// <summary>
|
||||
/// Generates a weekly performance report from an Excel file in tmp_report/{installationId}.xlsx
|
||||
/// Returns JSON with daily data, weekly totals, ratios, and AI insight.
|
||||
/// Returns a weekly performance report. Serves from cache if available;
|
||||
/// generates fresh on first request or when forceRegenerate is true.
|
||||
/// </summary>
|
||||
[HttpGet(nameof(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;
|
||||
if (user == null)
|
||||
|
|
@ -948,7 +949,40 @@ public class Controller : ControllerBase
|
|||
|
||||
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(
|
||||
installationId, installation.InstallationName, lang, weekStartDate);
|
||||
|
||||
|
|
|
|||
|
|
@ -43,6 +43,12 @@ public class WeeklyReportSummary
|
|||
// AI insight for this week
|
||||
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; } = "";
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -77,6 +77,18 @@ public static partial class Db
|
|||
.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)
|
||||
=> MonthlyReports
|
||||
.Where(r => r.InstallationId == installationId)
|
||||
|
|
|
|||
|
|
@ -185,6 +185,29 @@ public static class ReportAggregationService
|
|||
foreach (var old in overlapping)
|
||||
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
|
||||
{
|
||||
InstallationId = installationId,
|
||||
|
|
@ -207,6 +230,7 @@ public static class ReportAggregationService
|
|||
WeekdayAvgDailyLoad = report.Behavior?.WeekdayAvgDailyLoad ?? 0,
|
||||
WeekendAvgDailyLoad = report.Behavior?.WeekendAvgDailyLoad ?? 0,
|
||||
AiInsight = report.AiInsight,
|
||||
ResponseJson = JsonConvert.SerializeObject(reportForCache),
|
||||
CreatedAt = DateTime.UtcNow.ToString("o"),
|
||||
};
|
||||
|
||||
|
|
@ -574,6 +598,25 @@ public static class ReportAggregationService
|
|||
() => 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>
|
||||
public static Task<String> GetOrGenerateMonthlyInsightAsync(
|
||||
MonthlyReportSummary report, String language)
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ import {
|
|||
} from '@mui/material';
|
||||
import SendIcon from '@mui/icons-material/Send';
|
||||
import DownloadIcon from '@mui/icons-material/Download';
|
||||
import RefreshIcon from '@mui/icons-material/Refresh';
|
||||
import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
|
||||
import axiosConfig from 'src/Resources/axiosConfig';
|
||||
|
||||
|
|
@ -514,12 +515,12 @@ function WeeklySection({ installationId, latestMonthlyPeriodEnd }: { installatio
|
|||
fetchReport();
|
||||
}, [installationId, intl.locale]);
|
||||
|
||||
const fetchReport = async () => {
|
||||
const fetchReport = async (forceRegenerate = false) => {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
try {
|
||||
const res = await axiosConfig.get('/GetWeeklyReport', {
|
||||
params: { installationId, language: intl.locale }
|
||||
params: { installationId, language: intl.locale, forceRegenerate }
|
||||
});
|
||||
setReport(res.data);
|
||||
} catch (err: any) {
|
||||
|
|
@ -614,15 +615,28 @@ function WeeklySection({ installationId, latestMonthlyPeriodEnd }: { installatio
|
|||
borderRadius: 2
|
||||
}}
|
||||
>
|
||||
<Typography variant="h5" fontWeight="bold">
|
||||
<FormattedMessage id="reportTitle" defaultMessage="Weekly Performance Report" />
|
||||
</Typography>
|
||||
<Typography variant="body1" sx={{ opacity: 0.9, mt: 0.5 }}>
|
||||
{report.installationName}
|
||||
</Typography>
|
||||
<Typography variant="body2" sx={{ opacity: 0.7 }}>
|
||||
{report.periodStart} — {report.periodEnd}
|
||||
</Typography>
|
||||
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start' }}>
|
||||
<Box>
|
||||
<Typography variant="h5" fontWeight="bold">
|
||||
<FormattedMessage id="reportTitle" defaultMessage="Weekly Performance Report" />
|
||||
</Typography>
|
||||
<Typography variant="body1" sx={{ opacity: 0.9, mt: 0.5 }}>
|
||||
{report.installationName}
|
||||
</Typography>
|
||||
<Typography variant="body2" sx={{ opacity: 0.7 }}>
|
||||
{report.periodStart} — {report.periodEnd}
|
||||
</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>
|
||||
|
||||
{/* Weekly Insights */}
|
||||
|
|
|
|||
Loading…
Reference in New Issue