792 lines
75 KiB
C#
792 lines
75 KiB
C#
using System.Text.Json;
|
||
using InnovEnergy.App.Backend.DataTypes;
|
||
using InnovEnergy.Lib.Mailer;
|
||
using MailKit.Net.Smtp;
|
||
using MailKit.Security;
|
||
using MimeKit;
|
||
|
||
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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";
|
||
|
||
/// <summary>
|
||
/// 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.
|
||
/// </summary>
|
||
public static async Task SendReportEmailAsync(WeeklyReportResponse report, string recipientEmail, string language = "en", string customerName = null)
|
||
{
|
||
var strings = GetStrings(language);
|
||
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();
|
||
|
||
var from = new MailboxAddress(config.SenderName, config.SenderAddress);
|
||
var to = new MailboxAddress(recipientEmail, recipientEmail);
|
||
|
||
var msg = new MimeMessage
|
||
{
|
||
From = { from },
|
||
To = { to },
|
||
Subject = subject,
|
||
Body = new TextPart("html") { Text = html }
|
||
};
|
||
|
||
Console.WriteLine($"[ReportEmailService] SMTP: {config.SmtpUsername}@{config.SmtpServerUrl}:{config.SmtpPort}");
|
||
|
||
using var smtp = new SmtpClient();
|
||
await smtp.ConnectAsync(config.SmtpServerUrl, config.SmtpPort, SecureSocketOptions.StartTls);
|
||
await smtp.AuthenticateAsync(config.SmtpUsername, config.SmtpPassword);
|
||
await smtp.SendAsync(msg);
|
||
await smtp.DisconnectAsync(true);
|
||
|
||
Console.WriteLine($"[ReportEmailService] Report sent to {recipientEmail}");
|
||
}
|
||
|
||
private static async Task<MailerConfig> ReadMailerConfig()
|
||
{
|
||
await using var fileStream = File.OpenRead(MailerConfig.DefaultFile);
|
||
var config = await JsonSerializer.DeserializeAsync<MailerConfig>(fileStream);
|
||
return config ?? throw new InvalidOperationException("Failed to read MailerConfig.json");
|
||
}
|
||
|
||
// ── Translation strings ─────────────────────────────────────────────────
|
||
|
||
private record EmailStrings(
|
||
string Title,
|
||
string Insights,
|
||
string Summary,
|
||
string SavingsHeader,
|
||
string DailyBreakdown,
|
||
string Metric,
|
||
string ThisWeek,
|
||
string LastWeek,
|
||
string Change,
|
||
string PvProduction,
|
||
string Consumption,
|
||
string GridImport,
|
||
string GridExport,
|
||
string BatteryInOut,
|
||
string SolarEnergyUsed,
|
||
string StayedAtHome,
|
||
string EstMoneySaved,
|
||
string AtRate,
|
||
string SolarCoverage,
|
||
string FromSolar,
|
||
string BatteryEff,
|
||
string OutVsIn,
|
||
string Day,
|
||
string Load,
|
||
string GridIn,
|
||
string GridOut,
|
||
string BattInOut,
|
||
string Footer,
|
||
string FooterLink
|
||
);
|
||
|
||
private static EmailStrings GetStrings(string language) => language switch
|
||
{
|
||
"de" => new EmailStrings(
|
||
Title: "Wöchentlicher Leistungsbericht",
|
||
Insights: "Wöchentliche Erkenntnisse",
|
||
Summary: "Wöchentliche Zusammenfassung",
|
||
SavingsHeader: "Ihre Ersparnisse diese Woche",
|
||
DailyBreakdown: "Tägliche Aufschlüsselung (kWh)",
|
||
Metric: "Kennzahl",
|
||
ThisWeek: "Diese Woche",
|
||
LastWeek: "Letzte Woche",
|
||
Change: "Änderung",
|
||
PvProduction: "PV-Produktion",
|
||
Consumption: "Verbrauch",
|
||
GridImport: "Netzbezug",
|
||
GridExport: "Netzeinspeisung",
|
||
BatteryInOut: "Batterie Laden / Entladen",
|
||
SolarEnergyUsed: "Energie gespart",
|
||
StayedAtHome: "Solar + Batterie, nicht vom Netz",
|
||
EstMoneySaved: "Geschätzte Ersparnis",
|
||
AtRate: "bei 0.39 CHF/kWh",
|
||
SolarCoverage: "Energieunabhängigkeit",
|
||
FromSolar: "aus eigenem Solar + Batterie System",
|
||
BatteryEff: "Batterie-Eff.",
|
||
OutVsIn: "Entladung vs. Ladung",
|
||
Day: "Tag",
|
||
Load: "Last",
|
||
GridIn: "Netz Ein",
|
||
GridOut: "Netz Aus",
|
||
BattInOut: "Batt. Laden/Entl.",
|
||
Footer: "Erstellt von <strong style=\"color:#666\">inesco Energy Monitor</strong>",
|
||
FooterLink: "Detaillierte Berichte ansehen auf monitor.inesco.energy"
|
||
),
|
||
"fr" => new EmailStrings(
|
||
Title: "Rapport de performance hebdomadaire",
|
||
Insights: "Aperçus de la semaine",
|
||
Summary: "Résumé de la semaine",
|
||
SavingsHeader: "Vos économies cette semaine",
|
||
DailyBreakdown: "Détail quotidien (kWh)",
|
||
Metric: "Indicateur",
|
||
ThisWeek: "Cette semaine",
|
||
LastWeek: "Semaine dernière",
|
||
Change: "Variation",
|
||
PvProduction: "Production PV",
|
||
Consumption: "Consommation",
|
||
GridImport: "Import réseau",
|
||
GridExport: "Export réseau",
|
||
BatteryInOut: "Batterie Charge / Décharge",
|
||
SolarEnergyUsed: "Énergie économisée",
|
||
StayedAtHome: "solaire + batterie, non achetée au réseau",
|
||
EstMoneySaved: "Économies estimées",
|
||
AtRate: "à 0.39 CHF/kWh",
|
||
SolarCoverage: "Indépendance énergétique",
|
||
FromSolar: "de votre système solaire + batterie",
|
||
BatteryEff: "Eff. batterie",
|
||
OutVsIn: "décharge vs charge",
|
||
Day: "Jour",
|
||
Load: "Charge",
|
||
GridIn: "Réseau Ent.",
|
||
GridOut: "Réseau Sor.",
|
||
BattInOut: "Batt. Ch./Déch.",
|
||
Footer: "Généré par <strong style=\"color:#666\">inesco Energy Monitor</strong>",
|
||
FooterLink: "Consultez vos rapports détaillés sur monitor.inesco.energy"
|
||
),
|
||
"it" => new EmailStrings(
|
||
Title: "Rapporto settimanale delle prestazioni",
|
||
Insights: "Approfondimenti settimanali",
|
||
Summary: "Riepilogo settimanale",
|
||
SavingsHeader: "I tuoi risparmi questa settimana",
|
||
DailyBreakdown: "Dettaglio giornaliero (kWh)",
|
||
Metric: "Metrica",
|
||
ThisWeek: "Questa settimana",
|
||
LastWeek: "La settimana scorsa",
|
||
Change: "Variazione",
|
||
PvProduction: "Produzione PV",
|
||
Consumption: "Consumo",
|
||
GridImport: "Import dalla rete",
|
||
GridExport: "Export nella rete",
|
||
BatteryInOut: "Batteria Carica / Scarica",
|
||
SolarEnergyUsed: "Energia risparmiata",
|
||
StayedAtHome: "solare + batteria, non acquistata dalla rete",
|
||
EstMoneySaved: "Risparmio stimato",
|
||
AtRate: "a 0.39 CHF/kWh",
|
||
SolarCoverage: "Indipendenza energetica",
|
||
FromSolar: "dal proprio impianto solare + batteria",
|
||
BatteryEff: "Eff. batteria",
|
||
OutVsIn: "scarica vs carica",
|
||
Day: "Giorno",
|
||
Load: "Carico",
|
||
GridIn: "Rete Ent.",
|
||
GridOut: "Rete Usc.",
|
||
BattInOut: "Batt. Car./Sc.",
|
||
Footer: "Generato da <strong style=\"color:#666\">inesco Energy Monitor</strong>",
|
||
FooterLink: "Visualizza i tuoi report dettagliati su monitor.inesco.energy"
|
||
),
|
||
_ => new EmailStrings(
|
||
Title: "Weekly Performance Report",
|
||
Insights: "Weekly Insights",
|
||
Summary: "Weekly Summary",
|
||
SavingsHeader: "Your Savings This Week",
|
||
DailyBreakdown: "Daily Breakdown (kWh)",
|
||
Metric: "Metric",
|
||
ThisWeek: "This Week",
|
||
LastWeek: "Last Week",
|
||
Change: "Change",
|
||
PvProduction: "PV Production",
|
||
Consumption: "Consumption",
|
||
GridImport: "Grid Import",
|
||
GridExport: "Grid Export",
|
||
BatteryInOut: "Battery Charge / Discharge",
|
||
SolarEnergyUsed: "Energy Saved",
|
||
StayedAtHome: "solar + battery, not bought from grid",
|
||
EstMoneySaved: "Est. Money Saved",
|
||
AtRate: "at 0.39 CHF/kWh",
|
||
SolarCoverage: "Energy Independence",
|
||
FromSolar: "from your own solar + battery system",
|
||
BatteryEff: "Battery Eff.",
|
||
OutVsIn: "discharge vs charge",
|
||
Day: "Day",
|
||
Load: "Load",
|
||
GridIn: "Grid In",
|
||
GridOut: "Grid Out",
|
||
BattInOut: "Batt. Ch./Dis.",
|
||
Footer: "Generated by <strong style=\"color:#666\">inesco Energy Monitor</strong>",
|
||
FooterLink: "View your detailed reports at monitor.inesco.energy"
|
||
)
|
||
};
|
||
|
||
// ── HTML email template ─────────────────────────────────────────────
|
||
|
||
public static string BuildHtmlEmail(WeeklyReportResponse r, string language = "en", string customerName = null)
|
||
=> BuildHtmlEmail(r, GetStrings(language), customerName);
|
||
|
||
private static string BuildHtmlEmail(WeeklyReportResponse r, EmailStrings s, string customerName = null)
|
||
{
|
||
var cur = r.CurrentWeek;
|
||
var prev = r.PreviousWeek;
|
||
|
||
// Parse AI insight into <li> bullet points (split on newlines, strip leading "- " or "1. ", strip ** markdown)
|
||
var insightLines = r.AiInsight
|
||
.Split('\n', StringSplitOptions.RemoveEmptyEntries)
|
||
.Select(l => System.Text.RegularExpressions.Regex.Replace(l.Trim(), @"^[\d]+[.)]\s*|^[-*]\s*", "").Replace("**", ""))
|
||
.Where(l => l.Length > 0)
|
||
.ToList();
|
||
|
||
var insightHtml = insightLines.Count > 1
|
||
? "<ul style=\"margin:0;padding-left:20px\">" +
|
||
string.Join("", insightLines.Select(l => $"<li style=\"margin-bottom:8px;line-height:1.6\">{FormatInsightLine(l)}</li>")) +
|
||
"</ul>"
|
||
: $"<p style=\"margin:0;line-height:1.6\">{FormatInsightLine(r.AiInsight)}</p>";
|
||
|
||
// Detect which components are present across all daily data
|
||
var showPv = r.DailyData.Any(d => d.PvProduction > 0.1);
|
||
var showGrid = r.DailyData.Any(d => d.GridImport > 0.1);
|
||
|
||
// Daily rows — colorful bar chart (pixel widths, email-safe)
|
||
// Scale each day's bars so their combined total always fills maxBarPx (right-edge aligned).
|
||
// This replicates the web page's CSS flexbox flex-shrink:1 behaviour.
|
||
const int maxBarPx = 400;
|
||
|
||
var dailyRows = "";
|
||
foreach (var d in r.DailyData)
|
||
{
|
||
var dayName = DateTime.Parse(d.Date).ToString("ddd dd.MM");
|
||
var isCurrentWeek = string.Compare(d.Date, r.PeriodStart, StringComparison.Ordinal) >= 0;
|
||
var opacity = isCurrentWeek ? "1" : "0.55";
|
||
var fontWeight = isCurrentWeek ? "bold" : "normal";
|
||
var dayTotal = (showPv ? d.PvProduction : 0) + d.LoadConsumption + (showGrid ? d.GridImport : 0);
|
||
if (dayTotal < 0.1) dayTotal = 0.1;
|
||
var pvPx = showPv ? (int)(d.PvProduction / dayTotal * maxBarPx) : 0;
|
||
var ldPx = (int)(d.LoadConsumption / dayTotal * maxBarPx);
|
||
var giPx = showGrid ? (int)(d.GridImport / dayTotal * maxBarPx) : 0;
|
||
|
||
var pvSpan = showPv ? $@"<span style=""display:inline-block;height:14px;background:#f39c12;width:{pvPx}px;border-radius:2px 0 0 2px""></span>" : "";
|
||
var gridSpan = showGrid ? $@"<span style=""display:inline-block;height:14px;background:#e74c3c;width:{giPx}px;border-radius:0 2px 2px 0;margin-left:2px""></span>" : "";
|
||
var ldRadius = (!showPv ? "border-radius:2px 0 0 2px;" : "") + (!showGrid ? "border-radius:0 2px 2px 0;" : "");
|
||
|
||
var valueText = (showPv ? $"PV {d.PvProduction:F1} | " : "")
|
||
+ $"{s.Load} {d.LoadConsumption:F1}"
|
||
+ (showGrid ? $" | {s.GridIn} {d.GridImport:F1}" : "")
|
||
+ " kWh";
|
||
|
||
dailyRows += $@"
|
||
<tr style=""opacity:{opacity};border-bottom:1px solid #f0f0f0"">
|
||
<td style=""padding:6px 8px;font-size:12px;font-weight:{fontWeight};white-space:nowrap;width:80px;vertical-align:top;padding-top:10px"">{dayName}</td>
|
||
<td style=""padding:4px 8px"">
|
||
<div style=""font-size:10px;color:#888;margin-bottom:3px;text-align:right"">{valueText}</div>
|
||
<div style=""height:14px;line-height:14px;font-size:0;white-space:nowrap;width:{maxBarPx}px"">{pvSpan}<span style=""display:inline-block;height:14px;background:#3498db;width:{ldPx}px;{ldRadius}margin-left:{(showPv ? 2 : 0)}px""></span>{gridSpan}</div>
|
||
</td>
|
||
</tr>";
|
||
}
|
||
|
||
// Week-over-week comparison rows
|
||
var comparisonHtml = prev != null
|
||
? $@"
|
||
<tr>
|
||
<td style=""padding:8px 12px;border-bottom:1px solid #eee"">{s.PvProduction}</td>
|
||
<td style=""padding:8px 12px;border-bottom:1px solid #eee;text-align:right;font-weight:bold"">{cur.TotalPvProduction:F1} kWh</td>
|
||
<td style=""padding:8px 12px;border-bottom:1px solid #eee;text-align:right;color:#888"">{prev.TotalPvProduction:F1} kWh</td>
|
||
<td style=""padding:8px 12px;border-bottom:1px solid #eee;text-align:right;color:{ChangeColor(r.PvChangePercent)}"">{FormatChange(r.PvChangePercent)}</td>
|
||
</tr>
|
||
<tr>
|
||
<td style=""padding:8px 12px;border-bottom:1px solid #eee"">{s.Consumption}</td>
|
||
<td style=""padding:8px 12px;border-bottom:1px solid #eee;text-align:right;font-weight:bold"">{cur.TotalConsumption:F1} kWh</td>
|
||
<td style=""padding:8px 12px;border-bottom:1px solid #eee;text-align:right;color:#888"">{prev.TotalConsumption:F1} kWh</td>
|
||
<td style=""padding:8px 12px;border-bottom:1px solid #eee;text-align:right;color:{ChangeColor(-r.ConsumptionChangePercent)}"">{FormatChange(r.ConsumptionChangePercent)}</td>
|
||
</tr>
|
||
<tr>
|
||
<td style=""padding:8px 12px;border-bottom:1px solid #eee"">{s.GridImport}</td>
|
||
<td style=""padding:8px 12px;border-bottom:1px solid #eee;text-align:right;font-weight:bold"">{cur.TotalGridImport:F1} kWh</td>
|
||
<td style=""padding:8px 12px;border-bottom:1px solid #eee;text-align:right;color:#888"">{prev.TotalGridImport:F1} kWh</td>
|
||
<td style=""padding:8px 12px;border-bottom:1px solid #eee;text-align:right;color:{ChangeColor(-r.GridImportChangePercent)}"">{FormatChange(r.GridImportChangePercent)}</td>
|
||
</tr>
|
||
<tr>
|
||
<td style=""padding:8px 12px;border-bottom:1px solid #eee"">{s.GridExport}</td>
|
||
<td style=""padding:8px 12px;border-bottom:1px solid #eee;text-align:right;font-weight:bold"">{cur.TotalGridExport:F1} kWh</td>
|
||
<td style=""padding:8px 12px;border-bottom:1px solid #eee;text-align:right;color:#888"">{prev.TotalGridExport:F1} kWh</td>
|
||
<td style=""padding:8px 12px;border-bottom:1px solid #eee;text-align:right"">—</td>
|
||
</tr>
|
||
<tr>
|
||
<td style=""padding:8px 12px;border-bottom:1px solid #eee"">{s.BatteryInOut}</td>
|
||
<td style=""padding:8px 12px;border-bottom:1px solid #eee;text-align:right;font-weight:bold"">{cur.TotalBatteryCharged:F1}/{cur.TotalBatteryDischarged:F1} kWh</td>
|
||
<td style=""padding:8px 12px;border-bottom:1px solid #eee;text-align:right;color:#888"">{prev.TotalBatteryCharged:F1}/{prev.TotalBatteryDischarged:F1} kWh</td>
|
||
<td style=""padding:8px 12px;border-bottom:1px solid #eee;text-align:right"">—</td>
|
||
</tr>"
|
||
: $@"
|
||
<tr><td style=""padding:8px 12px"">{s.PvProduction}</td><td style=""padding:8px 12px;text-align:right;font-weight:bold"">{cur.TotalPvProduction:F1} kWh</td></tr>
|
||
<tr><td style=""padding:8px 12px"">{s.Consumption}</td><td style=""padding:8px 12px;text-align:right;font-weight:bold"">{cur.TotalConsumption:F1} kWh</td></tr>
|
||
<tr><td style=""padding:8px 12px"">{s.GridImport}</td><td style=""padding:8px 12px;text-align:right;font-weight:bold"">{cur.TotalGridImport:F1} kWh</td></tr>
|
||
<tr><td style=""padding:8px 12px"">{s.GridExport}</td><td style=""padding:8px 12px;text-align:right;font-weight:bold"">{cur.TotalGridExport:F1} kWh</td></tr>
|
||
<tr><td style=""padding:8px 12px"">{s.BatteryInOut}</td><td style=""padding:8px 12px;text-align:right;font-weight:bold"">{cur.TotalBatteryCharged:F1}/{cur.TotalBatteryDischarged:F1} kWh</td></tr>";
|
||
|
||
var comparisonHeaders = prev != null
|
||
? $@"<th style=""padding:8px 12px;text-align:right"">{s.ThisWeek}</th>
|
||
<th style=""padding:8px 12px;text-align:right"">{s.LastWeek}</th>
|
||
<th style=""padding:8px 12px;text-align:right"">{s.Change}</th>"
|
||
: $@"<th style=""padding:8px 12px;text-align:right"">{s.ThisWeek}</th>";
|
||
|
||
return $@"
|
||
<!DOCTYPE html>
|
||
<html>
|
||
<head><meta charset=""utf-8""></head>
|
||
<body style=""margin:0;padding:0;background:#f4f4f4;font-family:Arial,Helvetica,sans-serif;font-size:14px;color:#333"">
|
||
<table width=""100%"" cellpadding=""0"" cellspacing=""0"" style=""background:#f4f4f4;padding:20px 0"">
|
||
<tr><td align=""center"">
|
||
<table width=""600"" cellpadding=""0"" cellspacing=""0"" style=""background:#ffffff;border-radius:8px;overflow:hidden;box-shadow:0 2px 8px rgba(0,0,0,0.08)"">
|
||
|
||
<!-- Header -->
|
||
<tr>
|
||
<td style=""background:#2c3e50;padding:24px 30px;color:#ffffff"">
|
||
<img src=""{LogoBase64}"" alt=""inesco Energy"" style=""height:36px;margin-bottom:12px"" />
|
||
<div style=""font-size:20px;font-weight:bold"">{s.Title}</div>
|
||
<div style=""font-size:14px;margin-top:6px;opacity:0.9"">{r.InstallationName}</div>
|
||
<div style=""font-size:13px;margin-top:2px;opacity:0.7"">{r.PeriodStart} — {r.PeriodEnd}</div>
|
||
</td>
|
||
</tr>
|
||
|
||
<!-- Weekly Insights (top) -->
|
||
<tr>
|
||
<td style=""padding:24px 30px 0"">
|
||
<div style=""font-size:16px;font-weight:bold;margin-bottom:12px;color:#2c3e50"">{s.Insights}</div>
|
||
<div style=""background:#fef9e7;border-left:4px solid #f39c12;padding:14px 18px;border-radius:0 6px 6px 0;font-size:14px;color:#333"">
|
||
{insightHtml}
|
||
</div>
|
||
</td>
|
||
</tr>
|
||
|
||
<!-- Weekly Totals -->
|
||
<tr>
|
||
<td style=""padding:24px 30px"">
|
||
<div style=""font-size:16px;font-weight:bold;margin-bottom:12px;color:#2c3e50"">{s.Summary}</div>
|
||
<table width=""100%"" cellpadding=""0"" cellspacing=""0"" style=""border:1px solid #eee;border-radius:4px"">
|
||
<tr style=""background:#f8f9fa"">
|
||
<th style=""padding:8px 12px;text-align:left"">{s.Metric}</th>
|
||
{comparisonHeaders}
|
||
</tr>
|
||
{comparisonHtml}
|
||
</table>
|
||
</td>
|
||
</tr>
|
||
|
||
<!-- Key Ratios -->
|
||
<tr>
|
||
<td style=""padding:0 30px 24px"">
|
||
<div style=""font-size:16px;font-weight:bold;margin-bottom:12px;color:#2c3e50"">{s.SavingsHeader}</div>
|
||
<table width=""100%"" cellpadding=""0"" cellspacing=""8"">
|
||
<tr>
|
||
{SavingsBox(s.SolarEnergyUsed, $"{r.TotalEnergySaved:F1} kWh", s.StayedAtHome, "#27ae60")}
|
||
{SavingsBox(s.EstMoneySaved, $"~{r.TotalSavingsCHF:F0} CHF", s.AtRate, "#2980b9")}
|
||
{SavingsBox(s.SolarCoverage, $"{r.SelfSufficiencyPercent:F0}%", s.FromSolar, "#8e44ad")}
|
||
{SavingsBox(s.BatteryEff, $"{r.BatteryEfficiencyPercent:F0}%", s.OutVsIn, "#e67e22")}
|
||
</tr>
|
||
</table>
|
||
</td>
|
||
</tr>
|
||
|
||
<!-- Daily Breakdown (bar chart) -->
|
||
<tr>
|
||
<td style=""padding:0 30px 24px"">
|
||
<div style=""font-size:16px;font-weight:bold;margin-bottom:8px;color:#2c3e50"">{s.DailyBreakdown}</div>
|
||
<table width=""100%"" cellpadding=""0"" cellspacing=""0"" style=""border:1px solid #eee;border-radius:4px;font-size:13px"">
|
||
<!-- Legend -->
|
||
<tr style=""background:#f8f9fa"">
|
||
<td colspan=""2"" style=""padding:8px 10px;font-size:12px"">
|
||
{(showPv ? @$"<span style=""display:inline-block;width:10px;height:10px;background:#f39c12;border-radius:2px;margin-right:4px""></span>PV " : "")}
|
||
<span style=""display:inline-block;width:10px;height:10px;background:#3498db;border-radius:2px;margin-right:4px""></span>{s.Load}
|
||
{(showGrid ? @$"<span style=""display:inline-block;width:10px;height:10px;background:#e74c3c;border-radius:2px;margin-right:4px""></span>{s.GridIn}" : "")}
|
||
</td>
|
||
</tr>
|
||
{dailyRows}
|
||
</table>
|
||
</td>
|
||
</tr>
|
||
|
||
<!-- Footer -->
|
||
<tr>
|
||
<td style=""background:#f8f9fa;padding:16px 30px;text-align:center;font-size:12px;color:#999;border-top:1px solid #eee"">
|
||
{s.Footer}
|
||
<div style=""margin-top:10px""><a href=""https://monitor.inesco.energy"" style=""color:#999;text-decoration:underline"">{s.FooterLink}</a></div>
|
||
</td>
|
||
</tr>
|
||
|
||
</table>
|
||
</td></tr>
|
||
</table>
|
||
</body>
|
||
</html>";
|
||
}
|
||
|
||
private static string SavingsBox(string label, string value, string subtitle, string color) =>
|
||
$@"<td width=""25%"" style=""text-align:center"">
|
||
<div style=""background:#f8f9fa;border-radius:6px;padding:12px 4px"">
|
||
<div style=""font-size:22px;font-weight:bold;color:{color}"">{value}</div>
|
||
<div style=""font-size:12px;font-weight:bold;color:#444;margin-top:4px"">{label}</div>
|
||
<div style=""font-size:10px;color:#888;margin-top:2px"">{subtitle}</div>
|
||
</div>
|
||
</td>";
|
||
|
||
// Bolds "Title" before first colon, and numbers+units in the rest
|
||
private static string FormatInsightLine(string line)
|
||
{
|
||
var colonIdx = line.IndexOf(':');
|
||
string result;
|
||
if (colonIdx > 0)
|
||
{
|
||
var title = line[..colonIdx];
|
||
var rest = line[colonIdx..]; // includes the colon
|
||
result = $"<strong>{title}</strong>{rest}";
|
||
}
|
||
else
|
||
{
|
||
result = line;
|
||
}
|
||
// Bold all numbers: time ranges (14:00–18:00), times (09:00), decimals, integers
|
||
result = System.Text.RegularExpressions.Regex.Replace(
|
||
result,
|
||
@"(\d{1,2}:\d{2}(?:[–\-]\d{1,2}:\d{2})?|\d+[.,]\d+|\d+)",
|
||
"<strong>$1</strong>");
|
||
return result;
|
||
}
|
||
|
||
private static string FormatChange(double pct) =>
|
||
pct == 0 ? "—" : pct > 0 ? $"+{pct:F1}%" : $"{pct:F1}%";
|
||
|
||
private static string ChangeColor(double pct) =>
|
||
pct > 0 ? "#27ae60" : pct < 0 ? "#e74c3c" : "#888";
|
||
|
||
// ── Monthly / Yearly Report Emails ────────────────────────────────────
|
||
|
||
private static readonly string[] MonthNamesEn = { "", "January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December" };
|
||
private static readonly string[] MonthNamesDe = { "", "Januar", "Februar", "März", "April", "Mai", "Juni", "Juli", "August", "September", "Oktober", "November", "Dezember" };
|
||
private static readonly string[] MonthNamesFr = { "", "Janvier", "Février", "Mars", "Avril", "Mai", "Juin", "Juillet", "Août", "Septembre", "Octobre", "Novembre", "Décembre" };
|
||
private static readonly string[] MonthNamesIt = { "", "Gennaio", "Febbraio", "Marzo", "Aprile", "Maggio", "Giugno", "Luglio", "Agosto", "Settembre", "Ottobre", "Novembre", "Dicembre" };
|
||
|
||
public static async Task SendMonthlyReportEmailAsync(
|
||
MonthlyReportSummary report,
|
||
string installationName,
|
||
string recipientEmail,
|
||
string language = "en",
|
||
string customerName = null)
|
||
{
|
||
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 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,
|
||
report.SelfSufficiencyPercent, report.BatteryEfficiencyPercent, report.AiInsight,
|
||
$"{report.WeekCount} {s.CountLabel}", s, customerName);
|
||
|
||
await SendHtmlEmailAsync(subject, html, recipientEmail);
|
||
}
|
||
|
||
public static async Task SendYearlyReportEmailAsync(
|
||
YearlyReportSummary report,
|
||
string installationName,
|
||
string recipientEmail,
|
||
string language = "en",
|
||
string customerName = null)
|
||
{
|
||
var s = GetAggregatedStrings(language, "yearly");
|
||
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,
|
||
report.SelfSufficiencyPercent, report.BatteryEfficiencyPercent, report.AiInsight,
|
||
$"{report.MonthCount} {s.CountLabel}", s, customerName);
|
||
|
||
await SendHtmlEmailAsync(subject, html, recipientEmail);
|
||
}
|
||
|
||
private static async Task SendHtmlEmailAsync(string subject, string html, string recipientEmail)
|
||
{
|
||
var config = await ReadMailerConfig();
|
||
var from = new MailboxAddress(config.SenderName, config.SenderAddress);
|
||
var to = new MailboxAddress(recipientEmail, recipientEmail);
|
||
|
||
var msg = new MimeMessage
|
||
{
|
||
From = { from },
|
||
To = { to },
|
||
Subject = subject,
|
||
Body = new TextPart("html") { Text = html }
|
||
};
|
||
|
||
using var smtp = new SmtpClient();
|
||
await smtp.ConnectAsync(config.SmtpServerUrl, config.SmtpPort, SecureSocketOptions.StartTls);
|
||
await smtp.AuthenticateAsync(config.SmtpUsername, config.SmtpPassword);
|
||
await smtp.SendAsync(msg);
|
||
await smtp.DisconnectAsync(true);
|
||
|
||
Console.WriteLine($"[ReportEmailService] Report sent to {recipientEmail}");
|
||
}
|
||
|
||
// ── Aggregated report translation strings ─────────────────────────────
|
||
|
||
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,
|
||
string SolarEnergyUsed, string StayedAtHome, string EstMoneySaved,
|
||
string AtRate, string SolarCoverage, string FromSolar,
|
||
string BatteryEff, string OutVsIn, string CountLabel, string Footer,
|
||
string FooterLink
|
||
);
|
||
|
||
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",
|
||
"Kennzahl", "Gesamt", "PV-Produktion", "Verbrauch", "Netzbezug", "Netzeinspeisung", "Batterie Laden / Entladen",
|
||
"Energie gespart", "Solar + Batterie, nicht vom Netz", "Geschätzte Ersparnis", "bei 0.39 CHF/kWh",
|
||
"Energieunabhängigkeit", "aus eigenem Solar + Batterie System", "Batterie-Eff.", "Entladung vs. Ladung",
|
||
"Tage aggregiert", "Erstellt von <strong style=\"color:#666\">inesco Energy Monitor</strong>",
|
||
"Detaillierte Berichte ansehen auf monitor.inesco.energy"),
|
||
("de", "yearly") => new AggregatedEmailStrings(
|
||
"Jährlicher Leistungsbericht", "Jährliche Erkenntnisse", "Jährliche Zusammenfassung", "Ihre Ersparnisse dieses Jahr",
|
||
"Kennzahl", "Gesamt", "PV-Produktion", "Verbrauch", "Netzbezug", "Netzeinspeisung", "Batterie Laden / Entladen",
|
||
"Energie gespart", "Solar + Batterie, nicht vom Netz", "Geschätzte Ersparnis", "bei 0.39 CHF/kWh",
|
||
"Energieunabhängigkeit", "aus eigenem Solar + Batterie System", "Batterie-Eff.", "Entladung vs. Ladung",
|
||
"Monate aggregiert", "Erstellt von <strong style=\"color:#666\">inesco Energy Monitor</strong>",
|
||
"Detaillierte Berichte ansehen auf monitor.inesco.energy"),
|
||
("fr", "monthly") => new AggregatedEmailStrings(
|
||
"Rapport de performance mensuel", "Aperçus du mois", "Résumé du mois", "Vos économies ce mois",
|
||
"Indicateur", "Total", "Production PV", "Consommation", "Import réseau", "Export réseau", "Batterie Charge / Décharge",
|
||
"Énergie économisée", "solaire + batterie, non achetée au réseau", "Économies estimées", "à 0.39 CHF/kWh",
|
||
"Indépendance énergétique", "de votre système solaire + batterie", "Eff. batterie", "décharge vs charge",
|
||
"jours agrégés", "Généré par <strong style=\"color:#666\">inesco Energy Monitor</strong>",
|
||
"Consultez vos rapports détaillés sur monitor.inesco.energy"),
|
||
("fr", "yearly") => new AggregatedEmailStrings(
|
||
"Rapport de performance annuel", "Aperçus de l'année", "Résumé de l'année", "Vos économies cette année",
|
||
"Indicateur", "Total", "Production PV", "Consommation", "Import réseau", "Export réseau", "Batterie Charge / Décharge",
|
||
"Énergie économisée", "solaire + batterie, non achetée au réseau", "Économies estimées", "à 0.39 CHF/kWh",
|
||
"Indépendance énergétique", "de votre système solaire + batterie", "Eff. batterie", "décharge vs charge",
|
||
"mois agrégés", "Généré par <strong style=\"color:#666\">inesco Energy Monitor</strong>",
|
||
"Consultez vos rapports détaillés sur monitor.inesco.energy"),
|
||
("it", "monthly") => new AggregatedEmailStrings(
|
||
"Rapporto mensile delle prestazioni", "Approfondimenti mensili", "Riepilogo mensile", "I tuoi risparmi questo mese",
|
||
"Metrica", "Totale", "Produzione PV", "Consumo", "Import dalla rete", "Export nella rete", "Batteria Carica / Scarica",
|
||
"Energia risparmiata", "solare + batteria, non acquistata dalla rete", "Risparmio stimato", "a 0.39 CHF/kWh",
|
||
"Indipendenza energetica", "dal proprio impianto solare + batteria", "Eff. batteria", "scarica vs carica",
|
||
"giorni aggregati", "Generato da <strong style=\"color:#666\">inesco Energy Monitor</strong>",
|
||
"Visualizza i tuoi report dettagliati su monitor.inesco.energy"),
|
||
("it", "yearly") => new AggregatedEmailStrings(
|
||
"Rapporto annuale delle prestazioni", "Approfondimenti annuali", "Riepilogo annuale", "I tuoi risparmi quest'anno",
|
||
"Metrica", "Totale", "Produzione PV", "Consumo", "Import dalla rete", "Export nella rete", "Batteria Carica / Scarica",
|
||
"Energia risparmiata", "solare + batteria, non acquistata dalla rete", "Risparmio stimato", "a 0.39 CHF/kWh",
|
||
"Indipendenza energetica", "dal proprio impianto solare + batteria", "Eff. batteria", "scarica vs carica",
|
||
"mesi aggregati", "Generato da <strong style=\"color:#666\">inesco Energy Monitor</strong>",
|
||
"Visualizza i tuoi report dettagliati su monitor.inesco.energy"),
|
||
(_, "monthly") => new AggregatedEmailStrings(
|
||
"Monthly Performance Report", "Monthly Insights", "Monthly Summary", "Your Savings This Month",
|
||
"Metric", "Total", "PV Production", "Consumption", "Grid Import", "Grid Export", "Battery Charge / Discharge",
|
||
"Energy Saved", "solar + battery, not bought from grid", "Est. Money Saved", "at 0.39 CHF/kWh",
|
||
"Energy Independence", "from your own solar + battery system", "Battery Eff.", "discharge vs charge",
|
||
"days aggregated", "Generated by <strong style=\"color:#666\">inesco Energy Monitor</strong>",
|
||
"View your detailed reports at monitor.inesco.energy"),
|
||
_ => new AggregatedEmailStrings(
|
||
"Annual Performance Report", "Annual Insights", "Annual Summary", "Your Savings This Year",
|
||
"Metric", "Total", "PV Production", "Consumption", "Grid Import", "Grid Export", "Battery Charge / Discharge",
|
||
"Energy Saved", "solar + battery, not bought from grid", "Est. Money Saved", "at 0.39 CHF/kWh",
|
||
"Energy Independence", "from your own solar + battery system", "Battery Eff.", "discharge vs charge",
|
||
"months aggregated", "Generated by <strong style=\"color:#666\">inesco Energy Monitor</strong>",
|
||
"View your detailed reports at monitor.inesco.energy")
|
||
};
|
||
|
||
// ── Aggregated HTML email template ────────────────────────────────────
|
||
|
||
// ── 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 $@"
|
||
<!DOCTYPE html>
|
||
<html>
|
||
<head><meta charset=""utf-8""></head>
|
||
<body style=""margin:0;padding:0;background:#f4f4f4;font-family:Arial,Helvetica,sans-serif;font-size:14px;color:#333"">
|
||
<table width=""100%"" cellpadding=""0"" cellspacing=""0"" style=""background:#f4f4f4;padding:20px 0"">
|
||
<tr><td align=""center"">
|
||
<table width=""600"" cellpadding=""0"" cellspacing=""0"" style=""background:#ffffff;border-radius:8px;overflow:hidden;box-shadow:0 2px 8px rgba(0,0,0,0.08)"">
|
||
|
||
<!-- Header -->
|
||
<tr>
|
||
<td style=""background:#2c3e50;padding:24px 30px;color:#ffffff"">
|
||
<img src=""{LogoBase64}"" alt=""inesco Energy"" style=""height:36px;margin-bottom:12px"" />
|
||
<div style=""font-size:20px;font-weight:bold"">{dailyTitle}</div>
|
||
<div style=""font-size:14px;margin-top:6px;opacity:0.9"">{installationName}</div>
|
||
<div style=""font-size:13px;margin-top:2px;opacity:0.7"">{record.Date}</div>
|
||
</td>
|
||
</tr>
|
||
|
||
<!-- Summary Table -->
|
||
<tr>
|
||
<td style=""padding:24px 30px"">
|
||
<div style=""font-size:16px;font-weight:bold;margin-bottom:12px;color:#2c3e50"">{s.Summary}</div>
|
||
<table width=""100%"" cellpadding=""0"" cellspacing=""0"" style=""border:1px solid #eee;border-radius:4px"">
|
||
<tr style=""background:#f8f9fa"">
|
||
<th style=""padding:8px 12px;text-align:left"">{s.Metric}</th>
|
||
<th style=""padding:8px 12px;text-align:right"">{s.Total}</th>
|
||
</tr>
|
||
<tr><td style=""padding:8px 12px;border-bottom:1px solid #eee"">{s.PvProduction}</td><td style=""padding:8px 12px;border-bottom:1px solid #eee;text-align:right;font-weight:bold"">{record.PvProduction:F1} kWh</td></tr>
|
||
<tr><td style=""padding:8px 12px;border-bottom:1px solid #eee"">{s.Consumption}</td><td style=""padding:8px 12px;border-bottom:1px solid #eee;text-align:right;font-weight:bold"">{record.LoadConsumption:F1} kWh</td></tr>
|
||
<tr><td style=""padding:8px 12px;border-bottom:1px solid #eee"">{s.GridImport}</td><td style=""padding:8px 12px;border-bottom:1px solid #eee;text-align:right;font-weight:bold"">{record.GridImport:F1} kWh</td></tr>
|
||
<tr><td style=""padding:8px 12px;border-bottom:1px solid #eee"">{s.GridExport}</td><td style=""padding:8px 12px;border-bottom:1px solid #eee;text-align:right;font-weight:bold"">{record.GridExport:F1} kWh</td></tr>
|
||
<tr><td style=""padding:8px 12px"">{s.BatteryInOut}</td><td style=""padding:8px 12px;text-align:right;font-weight:bold"">{record.BatteryCharged:F1} / {record.BatteryDischarged:F1} kWh</td></tr>
|
||
</table>
|
||
</td>
|
||
</tr>
|
||
|
||
<!-- Key Ratios -->
|
||
<tr>
|
||
<td style=""padding:0 30px 24px"">
|
||
<div style=""font-size:16px;font-weight:bold;margin-bottom:12px;color:#2c3e50"">{s.SavingsHeader}</div>
|
||
<table width=""100%"" cellpadding=""0"" cellspacing=""8"">
|
||
<tr>
|
||
{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")}
|
||
</tr>
|
||
</table>
|
||
</td>
|
||
</tr>
|
||
|
||
<!-- Footer -->
|
||
<tr>
|
||
<td style=""background:#f8f9fa;padding:16px 30px;text-align:center;font-size:12px;color:#999;border-top:1px solid #eee"">
|
||
{s.Footer}
|
||
<div style=""margin-top:10px""><a href=""https://monitor.inesco.energy"" style=""color:#999;text-decoration:underline"">{s.FooterLink}</a></div>
|
||
</td>
|
||
</tr>
|
||
|
||
</table>
|
||
</td></tr>
|
||
</table>
|
||
</body>
|
||
</html>";
|
||
}
|
||
|
||
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,
|
||
double selfSufficiency, double batteryEfficiency, string aiInsight,
|
||
string countLabel, AggregatedEmailStrings s, string customerName = null)
|
||
{
|
||
var insightLines = aiInsight
|
||
.Split('\n', StringSplitOptions.RemoveEmptyEntries)
|
||
.Select(l => System.Text.RegularExpressions.Regex.Replace(l.Trim(), @"^[\d]+[.)]\s*|^[-*]\s*", "").Replace("**", ""))
|
||
.Where(l => l.Length > 0)
|
||
.ToList();
|
||
|
||
var insightHtml = insightLines.Count > 1
|
||
? "<ul style=\"margin:0;padding-left:20px\">" +
|
||
string.Join("", insightLines.Select(l => $"<li style=\"margin-bottom:8px;line-height:1.6\">{FormatInsightLine(l)}</li>")) +
|
||
"</ul>"
|
||
: $"<p style=\"margin:0;line-height:1.6\">{FormatInsightLine(aiInsight)}</p>";
|
||
|
||
return $@"
|
||
<!DOCTYPE html>
|
||
<html>
|
||
<head><meta charset=""utf-8""></head>
|
||
<body style=""margin:0;padding:0;background:#f4f4f4;font-family:Arial,Helvetica,sans-serif;font-size:14px;color:#333"">
|
||
<table width=""100%"" cellpadding=""0"" cellspacing=""0"" style=""background:#f4f4f4;padding:20px 0"">
|
||
<tr><td align=""center"">
|
||
<table width=""600"" cellpadding=""0"" cellspacing=""0"" style=""background:#ffffff;border-radius:8px;overflow:hidden;box-shadow:0 2px 8px rgba(0,0,0,0.08)"">
|
||
|
||
<!-- Header -->
|
||
<tr>
|
||
<td style=""background:#2c3e50;padding:24px 30px;color:#ffffff"">
|
||
<img src=""{LogoBase64}"" alt=""inesco Energy"" style=""height:36px;margin-bottom:12px"" />
|
||
<div style=""font-size:20px;font-weight:bold"">{s.Title}</div>
|
||
<div style=""font-size:14px;margin-top:6px;opacity:0.9"">{installationName}</div>
|
||
<div style=""font-size:13px;margin-top:2px;opacity:0.7"">{periodStart} — {periodEnd}</div>
|
||
<div style=""font-size:12px;margin-top:2px;opacity:0.5"">{countLabel}</div>
|
||
</td>
|
||
</tr>
|
||
|
||
<!-- Insights -->
|
||
<tr>
|
||
<td style=""padding:24px 30px 0"">
|
||
<div style=""font-size:16px;font-weight:bold;margin-bottom:12px;color:#2c3e50"">{s.Insights}</div>
|
||
<div style=""background:#fef9e7;border-left:4px solid #f39c12;padding:14px 18px;border-radius:0 6px 6px 0;font-size:14px;color:#333"">
|
||
{insightHtml}
|
||
</div>
|
||
</td>
|
||
</tr>
|
||
|
||
<!-- Summary Table -->
|
||
<tr>
|
||
<td style=""padding:24px 30px"">
|
||
<div style=""font-size:16px;font-weight:bold;margin-bottom:12px;color:#2c3e50"">{s.Summary}</div>
|
||
<table width=""100%"" cellpadding=""0"" cellspacing=""0"" style=""border:1px solid #eee;border-radius:4px"">
|
||
<tr style=""background:#f8f9fa"">
|
||
<th style=""padding:8px 12px;text-align:left"">{s.Metric}</th>
|
||
<th style=""padding:8px 12px;text-align:right"">{s.Total}</th>
|
||
</tr>
|
||
<tr><td style=""padding:8px 12px;border-bottom:1px solid #eee"">{s.PvProduction}</td><td style=""padding:8px 12px;border-bottom:1px solid #eee;text-align:right;font-weight:bold"">{pvProduction:F1} kWh</td></tr>
|
||
<tr><td style=""padding:8px 12px;border-bottom:1px solid #eee"">{s.Consumption}</td><td style=""padding:8px 12px;border-bottom:1px solid #eee;text-align:right;font-weight:bold"">{consumption:F1} kWh</td></tr>
|
||
<tr><td style=""padding:8px 12px;border-bottom:1px solid #eee"">{s.GridImport}</td><td style=""padding:8px 12px;border-bottom:1px solid #eee;text-align:right;font-weight:bold"">{gridImport:F1} kWh</td></tr>
|
||
<tr><td style=""padding:8px 12px;border-bottom:1px solid #eee"">{s.GridExport}</td><td style=""padding:8px 12px;border-bottom:1px solid #eee;text-align:right;font-weight:bold"">{gridExport:F1} kWh</td></tr>
|
||
<tr><td style=""padding:8px 12px"">{s.BatteryInOut}</td><td style=""padding:8px 12px;text-align:right;font-weight:bold"">{batteryCharged:F1} / {batteryDischarged:F1} kWh</td></tr>
|
||
</table>
|
||
</td>
|
||
</tr>
|
||
|
||
<!-- Key Ratios -->
|
||
<tr>
|
||
<td style=""padding:0 30px 24px"">
|
||
<div style=""font-size:16px;font-weight:bold;margin-bottom:12px;color:#2c3e50"">{s.SavingsHeader}</div>
|
||
<table width=""100%"" cellpadding=""0"" cellspacing=""8"">
|
||
<tr>
|
||
{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")}
|
||
</tr>
|
||
</table>
|
||
</td>
|
||
</tr>
|
||
|
||
<!-- Footer -->
|
||
<tr>
|
||
<td style=""background:#f8f9fa;padding:16px 30px;text-align:center;font-size:12px;color:#999;border-top:1px solid #eee"">
|
||
{s.Footer}
|
||
<div style=""margin-top:10px""><a href=""https://monitor.inesco.energy"" style=""color:#999;text-decoration:underline"">{s.FooterLink}</a></div>
|
||
</td>
|
||
</tr>
|
||
|
||
</table>
|
||
</td></tr>
|
||
</table>
|
||
</body>
|
||
</html>";
|
||
}
|
||
}
|