diff --git a/csharp/App/Backend/Controller.cs b/csharp/App/Backend/Controller.cs index 5f38d6266..69d76a324 100644 --- a/csharp/App/Backend/Controller.cs +++ b/csharp/App/Backend/Controller.cs @@ -1471,6 +1471,91 @@ public class Controller : ControllerBase } } + // ── Report HTML (for PDF download) ───────────────────────────── + + [HttpGet(nameof(GetWeeklyReportHtml))] + public async Task 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 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 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>> GetWeeklyReportSummaries( Int64 installationId, Int32 year, Int32 month, Token authToken, String? language = null) diff --git a/csharp/App/Backend/Services/ReportEmailService.cs b/csharp/App/Backend/Services/ReportEmailService.cs index e484a7501..d0ef5c7bd 100644 --- a/csharp/App/Backend/Services/ReportEmailService.cs +++ b/csharp/App/Backend/Services/ReportEmailService.cs @@ -9,6 +9,11 @@ namespace InnovEnergy.App.Backend.Services; public static class ReportEmailService { + // inesco logo (dark background variant, SVG) embedded as base64 data URI for emails and PDF reports + private const string LogoBase64 = "data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz48c3ZnIGlkPSJf0KHQu9C+0LlfMSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB2aWV3Qm94PSIwIDAgNjY2LjMzIDIzNy44MyI+PGRlZnM+PHN0eWxlPi5jbHMtMXtmaWxsOiNmZmY7fS5jbHMtMntmaWxsOiMwMGIwNTA7fTwvc3R5bGU+PC9kZWZzPjxwYXRoIGNsYXNzPSJjbHMtMiIgZD0ibTUuMDksMzIuMjJDMS43LDI4LjgzLDAsMjQuMzEsMCwxOC42NVMxLjcsOC40OCw1LjA5LDUuMDlDOC40OCwxLjcsMTMuMjQsMCwxOS4zOCwwczEwLjY2LDEuNywxNC4wNSw1LjA5YzMuMzksMy4zOSw1LjA5LDcuOTEsNS4wOSwxMy41NnMtMS43LDEwLjE3LTUuMDksMTMuNTdjLTMuMzksMy4zOS04LjA4LDUuMDktMTQuMDUsNS4wOXMtMTAuOS0xLjctMTQuMjktNS4wOVoiLz48cmVjdCBjbGFzcz0iY2xzLTEiIHg9IjIuNjYiIHk9IjUzLjc4IiB3aWR0aD0iMzMuNDMiIGhlaWdodD0iMTIzLjc4Ii8+PHBhdGggY2xhc3M9ImNscy0xIiBkPSJtNjMuMTcsMTc3LjU2VjYxLjUzYzE2LjYzLTcuMTEsMzQuODgtMTAuNjYsNTQuNzUtMTAuNjYsMTcuNzYsMCwzMS40OSw0LjI4LDQxLjE4LDEyLjg0LDkuNjksOC41NiwxNC41NCwyMC44MywxNC41NCwzNi44MnY3Ny4wM2gtMzMuNDN2LTc0LjYxYzAtMTYuMzEtNy41MS0yNC40Ny0yMi41My0yNC40Ny03LjkxLDAtMTUuMDIsMS40NS0yMS4zMiw0LjM2djk0LjcyaC0zMy4xOVoiLz48cGF0aCBjbGFzcz0iY2xzLTEiIGQ9Im0zMDUuNDEsMTA1Ljg2YzAsNS44MS0uMzMsMTEuOTUtLjk3LDE4LjQxaC03Ni41NWMzLjA3LDE4Ljc0LDE0LjM3LDI4LjEsMzMuOTEsMjguMSwxMS4zLDAsMjIuNTMtMi4zNCwzMy42Ny03LjAzbDUuMzMsMjYuODljLTEyLjExLDUuNDktMjYuMjUsOC4yNC00Mi4zOSw4LjI0LTE5LjcsMC0zNS41Ny01Ljg1LTQ3LjYtMTcuNTYtMTIuMDMtMTEuNzEtMTguMDUtMjcuNDktMTguMDUtNDcuMzZzNS41Ny0zNC44LDE2LjcxLTQ2Ljc1YzExLjE0LTExLjk1LDI1LjU5LTE3LjkyLDQzLjM2LTE3LjkyLDE1LjgyLDAsMjguNTQsNS4wOSwzOC4xNSwxNS4yNiw5LjYxLDEwLjE3LDE0LjQxLDIzLjQyLDE0LjQxLDM5LjczWm0tMzEuOTgtMS45NHYtMi45MWMwLTYuOTQtMS44Mi0xMi42LTUuNDUtMTYuOTYtMy42My00LjM2LTguNjgtNi41NC0xNS4xNC02LjU0LTYuOTUsMC0xMi42NCwyLjM0LTE3LjA4LDcuMDItNC40NCw0LjY5LTcuMjMsMTEuMTQtOC4zNiwxOS4zOGg0Ni4wM1oiLz48cGF0aCBjbGFzcz0iY2xzLTEiIGQ9Im00MDMuNTYsODUuMDJjLTE0LjM3LTQuMzYtMjUuNi02LjU0LTMzLjY3LTYuNTQtMTEuMywwLTE2Ljk2LDMuMzEtMTYuOTYsOS45MywwLDMuMzksMS4zNyw1LjkzLDQuMTIsNy42MywyLjc0LDEuNyw4LjA3LDMuNjcsMTUuOTksNS45MywxNC42OSw0LjA0LDI1LjIzLDguOTIsMzEuNjEsMTQuNjYsNi4zOCw1Ljc0LDkuNTcsMTQuMDEsOS41NywyNC44MywwLDEyLjYtNC42NCwyMi4yNS0xMy45MywyOC45NS05LjI5LDYuNy0yMS42LDEwLjA1LTM2Ljk0LDEwLjA1LTE2Ljk2LDAtMzEuNTctMi42Ny00My44NS04bDQuNi0yNy4zN2MxMy40LDUuMTcsMjYuMjQsNy43NSwzOC41Miw3Ljc1LDUuNjUsMCwxMC4yMS0uOTMsMTMuNjktMi43OSwzLjQ3LTEuODUsNS4yMS00LjQ4LDUuMjEtNy44NywwLTMuNzEtMS42Ni02LjUtNC45Ny04LjM2LTMuMzEtMS44NS0xMC4yOS00LjMyLTIwLjk1LTcuMzktMTEuNzktMy4wNy0yMC42Ny03LjUxLTI2LjY1LTEzLjMyLTUuOTgtNS44MS04Ljk2LTEzLjgxLTguOTYtMjMuOTgsMC0xMi4yNyw0LjE2LTIxLjcyLDEyLjQ4LTI4LjM0LDguMzItNi42MiwxOS45LTkuOTMsMzQuNzYtOS45M3MyOC4xOCwyLjM0LDM5Ljk3LDcuMDJsLTMuNjMsMjcuMTNaIi8+PHBhdGggY2xhc3M9ImNscy0xIiBkPSJtNTIzLjYzLDU4LjE0bC02LjMsMjYuNGMtOS41My00LjAzLTE4LjY1LTYuMDYtMjcuMzctNi4wNi05LjY5LDAtMTcuMTYsMy4yMy0yMi40MSw5LjY5LTUuMjUsNi40Ni03Ljg3LDE1LjQyLTcuODcsMjYuODksMCwyNS4xOSwxMC42NiwzNy43OSwzMS45OCwzNy43OSw4LjU2LDAsMTcuNTItMi4xLDI2Ljg5LTYuM2w1LjMzLDI2Ljg5Yy05LjM3LDQuNjgtMjEsNy4wMy0zNC44OCw3LjAzLTIwLjAzLDAtMzUuNjEtNS43Ny00Ni43NS0xNy4zMi0xMS4xNC0xMS41NC0xNi43MS0yNy4yNS0xNi43MS00Ny4xMnM1LjQ5LTM1LjY5LDE2LjQ3LTQ3LjQ4YzEwLjk4LTExLjc5LDI2LTE3LjY4LDQ1LjA2LTE3LjY4LDEzLjg5LDAsMjYuMDgsMi40MiwzNi41OCw3LjI3WiIvPjxwYXRoIGNsYXNzPSJjbHMtMSIgZD0ibTYxOS4yNiwxMzkuNTRjLTEyLjQ1LDkuNDQtMzAuMjcsOC40OS00MS42My0yLjg3LTExLjM2LTExLjM2LTEyLjMxLTI5LjE3LTIuODctNDEuNjNsLTI0LjUtMjQuNWMtMjIuODUsMjYuMDMtMjEuODcsNjUuNjksMi45OCw5MC41MywyNC44NCwyNC44NCw2NC41LDI1LjgzLDkwLjUzLDIuOThsLTI0LjUxLTI0LjUxWiIvPjxyZWN0IGNsYXNzPSJjbHMtMiIgeD0iNTYzLjI1IiB5PSI2Mi4yNyIgd2lkdGg9IjQuMzciIGhlaWdodD0iMzQuNTEiIHRyYW5zZm9ybT0idHJhbnNsYXRlKDEwOS4zNyA0MjMuMTEpIHJvdGF0ZSgtNDUpIi8+PHJlY3QgY2xhc3M9ImNscy0yIiB4PSI1NjkuOCIgeT0iNTYuNzgiIHdpZHRoPSI0LjM3IiBoZWlnaHQ9IjM0LjUxIiB0cmFuc2Zvcm09InRyYW5zbGF0ZSg2MS4wMSAzNDEuNTMpIHJvdGF0ZSgtMzUuMDEpIi8+PHJlY3QgY2xhc3M9ImNscy0yIiB4PSI1NzcuMiIgeT0iNTIuNTEiIHdpZHRoPSI0LjM3IiBoZWlnaHQ9IjM0LjUxIiB0cmFuc2Zvcm09InRyYW5zbGF0ZSgyNC44NyAyNTEuNjEpIHJvdGF0ZSgtMjUuMDIpIi8+PHJlY3QgY2xhc3M9ImNscy0yIiB4PSI1ODUuMjMiIHk9IjQ5LjU5IiB3aWR0aD0iNC4zNyIgaGVpZ2h0PSIzNC41MSIgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoMi43MyAxNTQuNDIpIHJvdGF0ZSgtMTUuMDEpIi8+PHJlY3QgY2xhc3M9ImNscy0yIiB4PSI1OTMuNjUiIHk9IjQ4LjEiIHdpZHRoPSI0LjM3IiBoZWlnaHQ9IjM0LjUxIiB0cmFuc2Zvcm09InRyYW5zbGF0ZSgtMy40MyA1Mi4yMSkgcm90YXRlKC01KSIvPjxyZWN0IGNsYXNzPSJjbHMtMiIgeD0iNTg3LjEyIiB5PSI2My4xNyIgd2lkdGg9IjM0LjUxIiBoZWlnaHQ9IjQuMzciIHRyYW5zZm9ybT0idHJhbnNsYXRlKDQ4Ni41NSA2NjEuNzMpIHJvdGF0ZSgtODUpIi8+PHJlY3QgY2xhc3M9ImNscy0yIiB4PSI1OTUuNTQiIHk9IjY0LjY2IiB3aWR0aD0iMzQuNTEiIGhlaWdodD0iNC4zNyIgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoMzg5LjY0IDY0MS40Nikgcm90YXRlKC03NSkiLz48cmVjdCBjbGFzcz0iY2xzLTIiIHg9IjYwMy41NyIgeT0iNjcuNTgiIHdpZHRoPSIzNC41MSIgaGVpZ2h0PSI0LjM3IiB0cmFuc2Zvcm09InRyYW5zbGF0ZSgyOTUuMjkgNjAyLjk4KSByb3RhdGUoLTY1LjAxKSIvPjxyZWN0IGNsYXNzPSJjbHMtMiIgeD0iNjEwLjk3IiB5PSI3MS44NSIgd2lkdGg9IjM0LjUxIiBoZWlnaHQ9IjQuMzciIHRyYW5zZm9ybT0idHJhbnNsYXRlKDIwNy4zIDU0Ni4yMykgcm90YXRlKC01NS4wMSkiLz48cmVjdCBjbGFzcz0iY2xzLTIiIHg9IjYxNy41MiIgeT0iNzcuMzUiIHdpZHRoPSIzNC41MSIgaGVpZ2h0PSI0LjM3IiB0cmFuc2Zvcm09InRyYW5zbGF0ZSgxMjkuNjEgNDcyLjA1KSByb3RhdGUoLTQ0Ljk5KSIvPjxyZWN0IGNsYXNzPSJjbHMtMiIgeD0iNjIzLjAxIiB5PSI4My44OSIgd2lkdGg9IjM0LjUxIiBoZWlnaHQ9IjQuMzciIHRyYW5zZm9ybT0idHJhbnNsYXRlKDY2LjM0IDM4Mi42NSkgcm90YXRlKC0zNC45OCkiLz48cmVjdCBjbGFzcz0iY2xzLTIiIHg9IjYyNy4yOCIgeT0iOTEuMjkiIHdpZHRoPSIzNC41MSIgaGVpZ2h0PSI0LjM2IiB0cmFuc2Zvcm09InRyYW5zbGF0ZSgyMC45MyAyODEuMykgcm90YXRlKC0yNS4wMSkiLz48cmVjdCBjbGFzcz0iY2xzLTIiIHg9IjYzMC4yMSIgeT0iOTkuMzMiIHdpZHRoPSIzNC41MSIgaGVpZ2h0PSI0LjM2IiB0cmFuc2Zvcm09InRyYW5zbGF0ZSgtNC4yMSAxNzEuMDgpIHJvdGF0ZSgtMTUpIi8+PHJlY3QgY2xhc3M9ImNscy0yIiB4PSI2MzEuNjkiIHk9IjEwNy43NCIgd2lkdGg9IjM0LjUxIiBoZWlnaHQ9IjQuMzciIHRyYW5zZm9ybT0idHJhbnNsYXRlKC03LjEgNTYuODYpIHJvdGF0ZSgtNC45OSkiLz48cmVjdCBjbGFzcz0iY2xzLTIiIHg9IjY0Ni43NiIgeT0iMTAxLjIyIiB3aWR0aD0iNC4zNyIgaGVpZ2h0PSIzNC41MSIgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoNDc0LjQ3IDc1NC42NSkgcm90YXRlKC04NS4wMSkiLz48cmVjdCBjbGFzcz0iY2xzLTIiIHg9IjY0NS4yOCIgeT0iMTA5LjYzIiB3aWR0aD0iNC4zNyIgaGVpZ2h0PSIzNC41MSIgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoMzU3LjMgNzE5LjQzKSByb3RhdGUoLTc1KSIvPjxyZWN0IGNsYXNzPSJjbHMtMiIgeD0iNjQyLjM2IiB5PSIxMTcuNjYiIHdpZHRoPSI0LjM2IiBoZWlnaHQ9IjM0LjUxIiB0cmFuc2Zvcm09InRyYW5zbGF0ZSgyNDkuNzIgNjYxLjk0KSByb3RhdGUoLTY0Ljk4KSIvPjxwb2x5Z29uIGNsYXNzPSJjbHMtMiIgcG9pbnRzPSI2MjcuMzkgMTMwLjYzIDYyNC44OCAxMzQuMjEgNjUzLjE1IDE1NCA2NTUuNjUgMTUwLjQzIDYyNy4zOSAxMzAuNjMiLz48cG9seWdvbiBjbGFzcz0iY2xzLTIiIHBvaW50cz0iNjI0LjEyIDEzNS4xMiA2MjEuMDMgMTM4LjIxIDY0NS40MyAxNjIuNjEgNjQ4LjUxIDE1OS41MyA2MjQuMTIgMTM1LjEyIi8+PHBvbHlnb24gY2xhc3M9ImNscy0xIiBwb2ludHM9IjkuMTEgMjI0LjY3IDIzLjc4IDIyNC42NyAyMy43OCAyMTcuNzkgOS4xMSAyMTcuNzkgOS4xMSAyMTIuODEgMzQuMjggMjEyLjgxIDM0LjI4IDIwNS43OCAwIDIwNS43OCAwIDIzNy44MyAzNC42NCAyMzcuODMgMzQuNjQgMjMwLjU5IDkuMTEgMjMwLjU5IDkuMTEgMjI0LjY3Ii8+PHBvbHlnb24gY2xhc3M9ImNscy0xIiBwb2ludHM9IjE1Mi4xNiAyMjUuMTIgMTI3LjQ1IDIwNS43OCAxMjEuMjUgMjA1Ljc4IDEyMS4yNSAyMzcuODMgMTI5LjU3IDIzNy44MyAxMjkuNTcgMjE4LjIxIDE1NC4wNCAyMzcuNjUgMTU0LjI2IDIzNy44MyAxNjAuNDggMjM3LjgzIDE2MC40OCAyMDUuNzggMTUyLjE2IDIwNS43OCAxNTIuMTYgMjI1LjEyIi8+PHBvbHlnb24gY2xhc3M9ImNscy0xIiBwb2ludHM9IjI1Ni4yMSAyMjQuNjcgMjcwLjg3IDIyNC42NyAyNzAuODcgMjE3Ljc5IDI1Ni4yMSAyMTcuNzkgMjU2LjIxIDIxMi44MSAyODEuMzcgMjEyLjgxIDI4MS4zNyAyMDUuNzggMjQ3LjEgMjA1Ljc4IDI0Ny4xIDIzNy44MyAyODEuNzMgMjM3LjgzIDI4MS43MyAyMzAuNTkgMjU2LjIxIDIzMC41OSAyNTYuMjEgMjI0LjY3Ii8+PHBhdGggY2xhc3M9ImNscy0xIiBkPSJtMzk2LjU1LDIyNi4zMmMyLjY2LDAsNC41OC0uNDksNS44OS0xLjUsMS4zNi0xLjA0LDIuMDQtMi43OCwyLjA0LTUuMTR2LTcuMTljMC0yLjQxLS42OS00LjE3LTIuMDQtNS4yMS0xLjMxLTEuMDEtMy4yNC0xLjUtNS44OS0xLjVoLTI4LjIxdjMyLjA1aDguOTd2LTExLjVoNS44NGwxMi43MywxMS41aDEzLjI5bC0xNC40NS0xMS41aDEuODNabS0xOS4yNC0xMy42NmgxNi4wOGMxLjE5LDAsMS44Mi4yLDIuMTMuMzcuMjkuMTYuNDQuNTguNDQsMS4yNnYzLjUyYzAsLjY4LS4xNSwxLjEtLjQ0LDEuMjYtLjMxLjE3LS45My4zNy0yLjEzLjM3aC0xNi4wOHYtNi43OFoiLz48cGF0aCBjbGFzcz0iY2xzLTEiIGQ9Im01MzMuMTIsMjA3LjMxYy0xLjM2LTEuMDMtMy41Mi0xLjUzLTYuNjEtMS41M2gtMjJjLTEuNTMsMC0yLjg1LjExLTMuOTEuMzQtMS4xNC4yNC0yLjA4LjY4LTIuOCwxLjI5LS43NS42My0xLjI4LDEuNTEtMS41OSwyLjU5LS4yOSwxLjAxLS40MywyLjI1LS40MywzLjc4djE2LjAzYzAsMS41My4xNCwyLjc2LjQzLDMuNzUuMywxLjA1LjgyLDEuOTIsMS41MiwyLjU3LjcxLjY1LDEuNjUsMS4xLDIuNzksMS4zNSwxLjA2LjIzLDIuNC4zNCwzLjk4LjM0aDIyYzEuNTMsMCwyLjg2LS4xMSwzLjk0LS4zNCwxLjE3LS4yNCwyLjEyLS43LDIuODMtMS4zNS43MS0uNjUsMS4yMi0xLjUyLDEuNTItMi41Ny4yOC0uOTguNDMtMi4yNC40My0zLjc1di0xMS4zMWgtMjAuOTd2Ny4wM2gxMi4wOHY0Ljg0aC0yMS41MnYtMTcuMzVoMjEuNTJ2My4zOGw4Ljg5LTEuNXYtMS40OGMwLTMuMDMtLjY5LTUuMDMtMi4xMi02LjExWiIvPjxwb2x5Z29uIGNsYXNzPSJjbHMtMSIgcG9pbnRzPSI2NTUuMzIgMjA1Ljc4IDY0NC42NiAyMTYuNzggNjM0LjE1IDIwNS43OCA2MjEuODUgMjA1Ljc4IDYzOS42IDIyMy42OSA2MzkuNiAyMzcuODMgNjQ4LjY0IDIzNy44MyA2NDguNjQgMjIzLjU0IDY2Ni4zMyAyMDUuNzggNjU1LjMyIDIwNS43OCIvPjwvc3ZnPg=="; + + // NOTE: old PNG constant removed — using SVG dark-bg variant (data:image/png;base64,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"; + /// /// Sends the weekly report as a nicely formatted HTML email in the user's language. /// Uses MailKit directly (same config as existing Mailer library) but with HTML support. @@ -16,7 +21,8 @@ public static class ReportEmailService public static async Task SendReportEmailAsync(WeeklyReportResponse report, string recipientEmail, string language = "en", string customerName = null) { var strings = GetStrings(language); - var subject = $"{strings.Title} — {report.InstallationName} ({report.PeriodStart} to {report.PeriodEnd})"; + var installSegment = !string.IsNullOrWhiteSpace(report.InstallationName) ? $" — {report.InstallationName}" : ""; + var subject = $"{strings.Title}{installSegment} ({report.PeriodStart} to {report.PeriodEnd})"; var html = BuildHtmlEmail(report, strings, customerName); var config = await ReadMailerConfig(); @@ -334,6 +340,7 @@ public static class ReportEmailService +
{s.Title}
{r.InstallationName}
{r.PeriodStart} — {r.PeriodEnd}
@@ -467,7 +474,8 @@ public static class ReportEmailService var monthNames = language switch { "de" => MonthNamesDe, "fr" => MonthNamesFr, "it" => MonthNamesIt, _ => MonthNamesEn }; var monthName = report.Month >= 1 && report.Month <= 12 ? monthNames[report.Month] : report.Month.ToString(); var s = GetAggregatedStrings(language, "monthly"); - var subject = $"{s.Title} — {installationName} ({monthName} {report.Year})"; + var installSegment = !string.IsNullOrWhiteSpace(installationName) ? $" — {installationName}" : ""; + var subject = $"{s.Title}{installSegment} ({monthName} {report.Year})"; var html = BuildAggregatedHtmlEmail(report.PeriodStart, report.PeriodEnd, installationName, report.TotalPvProduction, report.TotalConsumption, report.TotalGridImport, report.TotalGridExport, report.TotalBatteryCharged, report.TotalBatteryDischarged, report.TotalEnergySaved, report.TotalSavingsCHF, @@ -485,7 +493,8 @@ public static class ReportEmailService string customerName = null) { var s = GetAggregatedStrings(language, "yearly"); - var subject = $"{s.Title} — {installationName} ({report.Year})"; + var installSegment = !string.IsNullOrWhiteSpace(installationName) ? $" — {installationName}" : ""; + var subject = $"{s.Title}{installSegment} ({report.Year})"; var html = BuildAggregatedHtmlEmail(report.PeriodStart, report.PeriodEnd, installationName, report.TotalPvProduction, report.TotalConsumption, report.TotalGridImport, report.TotalGridExport, report.TotalBatteryCharged, report.TotalBatteryDischarged, report.TotalEnergySaved, report.TotalSavingsCHF, @@ -520,7 +529,7 @@ public static class ReportEmailService // ── Aggregated report translation strings ───────────────────────────── - private record AggregatedEmailStrings( + public record AggregatedEmailStrings( string Title, string Insights, string Summary, string SavingsHeader, string Metric, string Total, string PvProduction, string Consumption, string GridImport, string GridExport, string BatteryInOut, @@ -530,7 +539,7 @@ public static class ReportEmailService string FooterLink ); - private static AggregatedEmailStrings GetAggregatedStrings(string language, string type) => (language, type) switch + public static AggregatedEmailStrings GetAggregatedStrings(string language, string type) => (language, type) switch { ("de", "monthly") => new AggregatedEmailStrings( "Monatlicher Leistungsbericht", "Monatliche Erkenntnisse", "Monatliche Zusammenfassung", "Ihre Ersparnisse diesen Monat", @@ -592,7 +601,98 @@ public static class ReportEmailService // ── Aggregated HTML email template ──────────────────────────────────── - private static string BuildAggregatedHtmlEmail( + // ── Daily Report HTML ──────────────────────────────────────────── + + public static string BuildDailyHtmlEmail( + DailyEnergyRecord record, string installationName, string language = "en") + { + var s = GetAggregatedStrings(language, "monthly"); // reuse monthly strings for metric labels + + var dailyTitle = language switch + { + "de" => "Täglicher Energiebericht", + "fr" => "Rapport énergétique quotidien", + "it" => "Rapporto energetico giornaliero", + _ => "Daily Energy Report" + }; + + var selfSufficiency = record.LoadConsumption > 0 + ? Math.Max(0, (1 - record.GridImport / record.LoadConsumption)) * 100 + : 0; + var batteryEfficiency = record.BatteryCharged > 0 + ? Math.Min(100, record.BatteryDischarged / record.BatteryCharged * 100) + : 0; + var energySaved = Math.Max(0, record.LoadConsumption - record.GridImport); + var savingsCHF = energySaved * 0.39; + + return $@" + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + +
+ +
{dailyTitle}
+
{installationName}
+
{record.Date}
+
+
{s.Summary}
+ + + + + + + + + + +
{s.Metric}{s.Total}
{s.PvProduction}{record.PvProduction:F1} kWh
{s.Consumption}{record.LoadConsumption:F1} kWh
{s.GridImport}{record.GridImport:F1} kWh
{s.GridExport}{record.GridExport:F1} kWh
{s.BatteryInOut}{record.BatteryCharged:F1} / {record.BatteryDischarged:F1} kWh
+
+
{s.SavingsHeader}
+ + + {SavingsBox(s.SolarEnergyUsed, $"{energySaved:F1} kWh", s.StayedAtHome, "#27ae60")} + {SavingsBox(s.EstMoneySaved, $"~{savingsCHF:F0} CHF", s.AtRate, "#2980b9")} + {SavingsBox(s.SolarCoverage, $"{selfSufficiency:F0}%", s.FromSolar, "#8e44ad")} + {SavingsBox(s.BatteryEff, $"{batteryEfficiency:F0}%", s.OutVsIn, "#e67e22")} + +
+
+ {s.Footer} + +
+
+ +"; + } + + public static string BuildAggregatedHtmlEmail( string periodStart, string periodEnd, string installationName, double pvProduction, double consumption, double gridImport, double gridExport, double batteryCharged, double batteryDischarged, double energySaved, double savingsCHF, @@ -623,6 +723,7 @@ public static class ReportEmailService +
{s.Title}
{installationName}
{periodStart} — {periodEnd}
diff --git a/typescript/frontend-marios2/src/Resources/images/inesco_logo_for_dark_bg.svg b/typescript/frontend-marios2/src/Resources/images/inesco_logo_for_dark_bg.svg new file mode 100644 index 000000000..c58778608 --- /dev/null +++ b/typescript/frontend-marios2/src/Resources/images/inesco_logo_for_dark_bg.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/typescript/frontend-marios2/src/content/dashboards/SodiohomeInstallations/DailySection.tsx b/typescript/frontend-marios2/src/content/dashboards/SodiohomeInstallations/DailySection.tsx index e21c151e4..c1206734e 100644 --- a/typescript/frontend-marios2/src/content/dashboards/SodiohomeInstallations/DailySection.tsx +++ b/typescript/frontend-marios2/src/content/dashboards/SodiohomeInstallations/DailySection.tsx @@ -97,10 +97,12 @@ function getCurrentWeekDays(currentMonday: Date): Date[] { export default function DailySection({ installationId, - onHasData + onHasData, + onPeriodChange }: { installationId: number; onHasData?: (hasData: boolean) => void; + onPeriodChange?: (date: string) => void; }) { const intl = useIntl(); const currentMonday = useMemo(() => getCurrentMonday(), []); @@ -113,7 +115,11 @@ export default function DailySection({ const [allRecords, setAllRecords] = useState([]); const [allHourlyRecords, setAllHourlyRecords] = useState([]); - const [selectedDate, setSelectedDate] = useState(formatDateISO(yesterday)); + const [selectedDate, setSelectedDate] = useState(() => { + const date = formatDateISO(yesterday); + onPeriodChange?.(date); + return date; + }); const [selectedDayRecord, setSelectedDayRecord] = useState(null); const [hourlyRecords, setHourlyRecords] = useState([]); const [loadingWeek, setLoadingWeek] = useState(false); @@ -174,6 +180,7 @@ export default function DailySection({ const handleStripSelect = (date: string) => { setSelectedDate(date); setNoData(false); + onPeriodChange?.(date); }; const dt = new Date(selectedDate + 'T00:00:00'); diff --git a/typescript/frontend-marios2/src/content/dashboards/SodiohomeInstallations/WeeklyReport.tsx b/typescript/frontend-marios2/src/content/dashboards/SodiohomeInstallations/WeeklyReport.tsx index 326621352..f19929a26 100644 --- a/typescript/frontend-marios2/src/content/dashboards/SodiohomeInstallations/WeeklyReport.tsx +++ b/typescript/frontend-marios2/src/content/dashboards/SodiohomeInstallations/WeeklyReport.tsx @@ -236,6 +236,8 @@ 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(null); const fetchReportData = () => { @@ -302,16 +304,56 @@ function WeeklyReport({ installationId }: WeeklyReportProps) { return false; })(); + const handleDownloadPdf = async () => { + const reportType = tabs[safeTab]?.key ?? 'report'; + let endpoint = ''; + const params: Record = { 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 ( - } - onClick={() => window.print()} + startIcon={downloadingPdf ? : } + onClick={handleDownloadPdf} + disabled={downloadingPdf} sx={{ ml: 2, whiteSpace: 'nowrap' }} > @@ -361,7 +404,7 @@ function WeeklyReport({ installationId }: WeeklyReportProps) { - + setReportPeriod({ start: date, end: date })} /> setReportPeriod({ start, end })} /> @@ -384,6 +428,7 @@ 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 })} /> @@ -395,6 +440,7 @@ function WeeklyReport({ installationId }: WeeklyReportProps) { onGenerate={handleGenerateYearly} selectedIdx={selectedYearlyIdx} onSelectedIdxChange={setSelectedYearlyIdx} + onPeriodChange={(r: YearlyReport) => setReportPeriod({ start: r.periodStart, end: r.periodEnd, year: r.year })} /> @@ -407,8 +453,8 @@ interface WeeklySectionHandle { regenerate: () => void; } -const WeeklySection = forwardRef void }>( - ({ installationId, latestMonthlyPeriodEnd, onHasData }, ref) => { +const WeeklySection = forwardRef void; onPeriodChange?: (start: string, end: string) => void }>( + ({ installationId, latestMonthlyPeriodEnd, onHasData, onPeriodChange }, ref) => { const intl = useIntl(); const [report, setReport] = useState(null); const [loading, setLoading] = useState(true); @@ -427,6 +473,7 @@ const WeeklySection = forwardRef void; selectedIdx: number; onSelectedIdxChange: (idx: number) => void; + onPeriodChange?: (report: MonthlyReport) => void; }) { const intl = useIntl(); @@ -871,6 +920,7 @@ function MonthlySection({ sendParamsFn={(r: MonthlyReport) => ({ installationId, year: r.year, month: r.month })} controlledIdx={selectedIdx} onIdxChange={onSelectedIdxChange} + onPeriodChange={onPeriodChange} /> ) : pendingMonths.length === 0 ? ( @@ -892,7 +942,8 @@ function YearlySection({ generating, onGenerate, selectedIdx, - onSelectedIdxChange + onSelectedIdxChange, + onPeriodChange }: { installationId: number; reports: YearlyReport[]; @@ -901,6 +952,7 @@ function YearlySection({ onGenerate: (year: number) => void; selectedIdx: number; onSelectedIdxChange: (idx: number) => void; + onPeriodChange?: (report: YearlyReport) => void; }) { const intl = useIntl(); @@ -952,6 +1004,7 @@ function YearlySection({ sendParamsFn={(r: YearlyReport) => ({ installationId, year: r.year })} controlledIdx={selectedIdx} onIdxChange={onSelectedIdxChange} + onPeriodChange={onPeriodChange} /> ) : pendingYears.length === 0 ? ( @@ -975,7 +1028,8 @@ function AggregatedSection({ sendEndpoint, sendParamsFn, controlledIdx, - onIdxChange + onIdxChange, + onPeriodChange }: { reports: T[]; type: 'monthly' | 'yearly'; @@ -986,6 +1040,7 @@ function AggregatedSection({ sendParamsFn: (r: T) => object; controlledIdx?: number; onIdxChange?: (idx: number) => void; + onPeriodChange?: (report: T) => void; }) { const intl = useIntl(); const [internalIdx, setInternalIdx] = useState(0); @@ -993,8 +1048,16 @@ function AggregatedSection({ 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 (