From b0bcf06d4edf24e6b8e8efb9b95469b1b475db51 Mon Sep 17 00:00:00 2001 From: Yinyin Liu Date: Sat, 28 Mar 2026 17:19:22 +0100 Subject: [PATCH] allow more flexible email frequency --- csharp/App/Backend/Controller.cs | 37 ++++++ .../App/Backend/DataTypes/EmailPreference.cs | 12 ++ csharp/App/Backend/DataTypes/Installation.cs | 1 + csharp/App/Backend/Database/Create.cs | 7 + csharp/App/Backend/Database/Db.cs | 3 + csharp/App/Backend/Database/Read.cs | 5 + .../Services/ReportAggregationService.cs | 82 ++++++++++++ .../Information/InformationSodistoreHome.tsx | 12 ++ .../SodiohomeInstallations/Installation.tsx | 1 + .../SodiohomeInstallations/WeeklyReport.tsx | 125 +++++++++++++----- .../src/interfaces/InstallationTypes.tsx | 1 + typescript/frontend-marios2/src/lang/de.json | 5 + typescript/frontend-marios2/src/lang/en.json | 5 + typescript/frontend-marios2/src/lang/fr.json | 5 + typescript/frontend-marios2/src/lang/it.json | 5 + 15 files changed, 276 insertions(+), 30 deletions(-) create mode 100644 csharp/App/Backend/DataTypes/EmailPreference.cs diff --git a/csharp/App/Backend/Controller.cs b/csharp/App/Backend/Controller.cs index 4f74271a2..28f1e1b3a 100644 --- a/csharp/App/Backend/Controller.cs +++ b/csharp/App/Backend/Controller.cs @@ -922,6 +922,43 @@ public class Controller : ControllerBase }); } + // ── Email Preferences ────────────────────────────────────────────── + + [HttpGet(nameof(GetEmailPreference))] + public ActionResult GetEmailPreference(Int64 installationId, Token authToken) + { + var user = Db.GetSession(authToken)?.User; + if (user == null) return Unauthorized(); + + var pref = Db.GetEmailPreference(installationId); + return Ok(new + { + installationId, + sendWeekly = pref?.SendWeekly ?? false, + sendMonthly = pref?.SendMonthly ?? false, + sendYearly = pref?.SendYearly ?? false + }); + } + + [HttpPost(nameof(UpdateEmailPreference))] + public ActionResult UpdateEmailPreference( + Int64 installationId, Boolean sendWeekly, Boolean sendMonthly, + Boolean sendYearly, Token authToken) + { + var user = Db.GetSession(authToken)?.User; + if (user == null) return Unauthorized(); + + Db.UpsertEmailPreference(new EmailPreference + { + InstallationId = installationId, + SendWeekly = sendWeekly, + SendMonthly = sendMonthly, + SendYearly = sendYearly + }); + + return Ok(); + } + // ── Weekly Performance Report ────────────────────────────────────── /// diff --git a/csharp/App/Backend/DataTypes/EmailPreference.cs b/csharp/App/Backend/DataTypes/EmailPreference.cs new file mode 100644 index 000000000..66e8348d1 --- /dev/null +++ b/csharp/App/Backend/DataTypes/EmailPreference.cs @@ -0,0 +1,12 @@ +using SQLite; + +namespace InnovEnergy.App.Backend.DataTypes; + +public class EmailPreference +{ + [PrimaryKey] + public Int64 InstallationId { get; set; } + public Boolean SendWeekly { get; set; } + public Boolean SendMonthly { get; set; } + public Boolean SendYearly { get; set; } +} diff --git a/csharp/App/Backend/DataTypes/Installation.cs b/csharp/App/Backend/DataTypes/Installation.cs index a7fb0fb39..54327005a 100644 --- a/csharp/App/Backend/DataTypes/Installation.cs +++ b/csharp/App/Backend/DataTypes/Installation.cs @@ -67,4 +67,5 @@ public class Installation : TreeNode public String VrmLink { get; set; } = ""; public string Configuration { get; set; } = ""; public string NetworkProvider { get; set; } = ""; + public string Email { get; set; } = ""; } \ No newline at end of file diff --git a/csharp/App/Backend/Database/Create.cs b/csharp/App/Backend/Database/Create.cs index b70a427ac..cef03a470 100644 --- a/csharp/App/Backend/Database/Create.cs +++ b/csharp/App/Backend/Database/Create.cs @@ -75,6 +75,13 @@ public static partial class Db public static Boolean Create(HourlyEnergyRecord record) => Insert(record); public static Boolean Create(AiInsightCache cache) => Insert(cache); + public static Boolean UpsertEmailPreference(EmailPreference pref) + { + var success = Connection.InsertOrReplace(pref) > 0; + if (success) Backup(); + return success; + } + // Ticket system public static Boolean Create(Ticket ticket) => Insert(ticket); public static Boolean Create(TicketComment comment) => Insert(comment); diff --git a/csharp/App/Backend/Database/Db.cs b/csharp/App/Backend/Database/Db.cs index 1350caa91..511f8aa80 100644 --- a/csharp/App/Backend/Database/Db.cs +++ b/csharp/App/Backend/Database/Db.cs @@ -31,6 +31,7 @@ public static partial class Db public static TableQuery DailyRecords => Connection.Table(); public static TableQuery HourlyRecords => Connection.Table(); public static TableQuery AiInsightCaches => Connection.Table(); + public static TableQuery EmailPreferences => Connection.Table(); // Ticket system tables public static TableQuery Tickets => Connection.Table(); @@ -69,6 +70,7 @@ public static partial class Db Connection.CreateTable(); Connection.CreateTable(); Connection.CreateTable(); + Connection.CreateTable(); // Ticket system tables Connection.CreateTable(); @@ -125,6 +127,7 @@ public static partial class Db fileConnection.CreateTable(); fileConnection.CreateTable(); fileConnection.CreateTable(); + fileConnection.CreateTable(); // Ticket system tables fileConnection.CreateTable(); diff --git a/csharp/App/Backend/Database/Read.cs b/csharp/App/Backend/Database/Read.cs index debcccb2e..2e14e363b 100644 --- a/csharp/App/Backend/Database/Read.cs +++ b/csharp/App/Backend/Database/Read.cs @@ -161,6 +161,11 @@ public static partial class Db && c.Language == language) ?.InsightText; + // ── EmailPreference Queries ───────────────────────────────────────── + + public static EmailPreference? GetEmailPreference(Int64 installationId) + => EmailPreferences.FirstOrDefault(p => p.InstallationId == installationId); + // ── Ticket Queries ────────────────────────────────────────────────── public static Ticket? GetTicketById(Int64 id) diff --git a/csharp/App/Backend/Services/ReportAggregationService.cs b/csharp/App/Backend/Services/ReportAggregationService.cs index f6f2041b4..bec798b34 100644 --- a/csharp/App/Backend/Services/ReportAggregationService.cs +++ b/csharp/App/Backend/Services/ReportAggregationService.cs @@ -121,6 +121,9 @@ public static class ReportAggregationService generated++; Console.WriteLine($"[ReportAggregation] Weekly report generated for installation {installation.Id} ({installation.Name})"); + + // Auto-send email if preference is set + await TryAutoSendWeeklyEmail(installation, report); } catch (Exception ex) { @@ -416,6 +419,9 @@ public static class ReportAggregationService }); Console.WriteLine($"[ReportAggregation] Monthly report created for installation {installationId}, {year}-{month:D2} ({days.Count} days, {first}–{last})."); + + // Auto-send email if preference is set + await TryAutoSendMonthlyEmail(installationId, monthlySummary); } // ── Year-End Aggregation ────────────────────────────────────────── @@ -529,6 +535,82 @@ public static class ReportAggregationService }); Console.WriteLine($"[ReportAggregation] Yearly report created for installation {installationId}, {year} ({monthlies.Count} months aggregated)."); + + // Auto-send email if preference is set + await TryAutoSendYearlyEmail(installationId, yearlySummary); + } + + // ── Auto-Send Email Helpers ───────────────────────────────────────── + + private static async Task TryAutoSendWeeklyEmail(Installation installation, WeeklyReportResponse report) + { + try + { + var pref = Db.GetEmailPreference(installation.Id); + if (pref is not { SendWeekly: true }) return; + + var email = installation.Email; + if (String.IsNullOrWhiteSpace(email)) + { + Console.WriteLine($"[AutoSend] Weekly: skipping installation {installation.Id} — no email configured."); + return; + } + + await ReportEmailService.SendReportEmailAsync(report, email, "en", installation.Name); + Console.WriteLine($"[AutoSend] Weekly email sent to {email} for installation {installation.Id}."); + } + catch (Exception ex) + { + Console.Error.WriteLine($"[AutoSend] Weekly email failed for installation {installation.Id}: {ex.Message}"); + } + } + + private static async Task TryAutoSendMonthlyEmail(Int64 installationId, MonthlyReportSummary report) + { + try + { + var pref = Db.GetEmailPreference(installationId); + if (pref is not { SendMonthly: true }) return; + + var installation = Db.GetInstallationById(installationId); + var email = installation?.Email; + if (String.IsNullOrWhiteSpace(email)) + { + Console.WriteLine($"[AutoSend] Monthly: skipping installation {installationId} — no email configured."); + return; + } + + await ReportEmailService.SendMonthlyReportEmailAsync(report, installation!.Name, email, "en"); + Console.WriteLine($"[AutoSend] Monthly email sent to {email} for installation {installationId}."); + } + catch (Exception ex) + { + Console.Error.WriteLine($"[AutoSend] Monthly email failed for installation {installationId}: {ex.Message}"); + } + } + + private static async Task TryAutoSendYearlyEmail(Int64 installationId, YearlyReportSummary report) + { + try + { + var pref = Db.GetEmailPreference(installationId); + if (pref is not { SendYearly: true }) return; + + var installation = Db.GetInstallationById(installationId); + var email = installation?.Email; + if (String.IsNullOrWhiteSpace(email)) + { + Console.WriteLine($"[AutoSend] Yearly: skipping installation {installationId} — no email configured."); + return; + } + + await ReportEmailService.SendYearlyReportEmailAsync(report, installation!.Name, email, "en"); + Console.WriteLine($"[AutoSend] Yearly email sent to {email} for installation {installationId}."); + } + catch (Exception ex) + { + Console.Error.WriteLine($"[AutoSend] Yearly email failed for installation {installationId}: {ex.Message}"); + } } // ── AI Insight Cache ────────────────────────────────────────────── diff --git a/typescript/frontend-marios2/src/content/dashboards/Information/InformationSodistoreHome.tsx b/typescript/frontend-marios2/src/content/dashboards/Information/InformationSodistoreHome.tsx index 9618d2a5a..937376d82 100644 --- a/typescript/frontend-marios2/src/content/dashboards/Information/InformationSodistoreHome.tsx +++ b/typescript/frontend-marios2/src/content/dashboards/Information/InformationSodistoreHome.tsx @@ -708,6 +708,18 @@ function InformationSodistorehome(props: InformationSodistorehomeProps) { /> +
+ } + name="email" + value={formValues.email || ''} + onChange={handleChange} + variant="outlined" + fullWidth + inputProps={{ readOnly: !canEdit }} + /> +
+
diff --git a/typescript/frontend-marios2/src/content/dashboards/SodiohomeInstallations/Installation.tsx b/typescript/frontend-marios2/src/content/dashboards/SodiohomeInstallations/Installation.tsx index 0257ec56a..e46776e68 100644 --- a/typescript/frontend-marios2/src/content/dashboards/SodiohomeInstallations/Installation.tsx +++ b/typescript/frontend-marios2/src/content/dashboards/SodiohomeInstallations/Installation.tsx @@ -619,6 +619,7 @@ function SodioHomeInstallation(props: singleInstallationProps) { } /> diff --git a/typescript/frontend-marios2/src/content/dashboards/SodiohomeInstallations/WeeklyReport.tsx b/typescript/frontend-marios2/src/content/dashboards/SodiohomeInstallations/WeeklyReport.tsx index 98209a2d1..ff662d40f 100644 --- a/typescript/frontend-marios2/src/content/dashboards/SodiohomeInstallations/WeeklyReport.tsx +++ b/typescript/frontend-marios2/src/content/dashboards/SodiohomeInstallations/WeeklyReport.tsx @@ -7,20 +7,24 @@ import { Alert, Box, Button, + Checkbox, CircularProgress, Container, + FormControlLabel, Grid, MenuItem, Paper, Select, + Snackbar, Tab, Tabs, TextField, + Tooltip, Typography } 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 SaveIcon from '@mui/icons-material/Save'; import ExpandMoreIcon from '@mui/icons-material/ExpandMore'; import axiosConfig from 'src/Resources/axiosConfig'; import DailySection from './DailySection'; @@ -28,6 +32,7 @@ import DailySection from './DailySection'; interface WeeklyReportProps { installationId: number; installationName?: string; + installationEmail?: string; } interface DailyEnergyData { @@ -224,7 +229,7 @@ function EmailBar({ onSend, disabled }: { onSend: (email: string) => Promise([]); @@ -234,13 +239,49 @@ function WeeklyReport({ installationId, installationName }: WeeklyReportProps) { const [generating, setGenerating] = useState(null); const [selectedMonthlyIdx, setSelectedMonthlyIdx] = useState(0); const [selectedYearlyIdx, setSelectedYearlyIdx] = useState(0); - const [regenerating, setRegenerating] = useState(false); const [dailyHasData, setDailyHasData] = useState(false); const [weeklyHasData, setWeeklyHasData] = useState(false); const [downloadingPdf, setDownloadingPdf] = useState(false); const [reportPeriod, setReportPeriod] = useState<{ start: string; end: string; year?: number; month?: number } | null>(null); const weeklyRef = useRef(null); + // Auto-send email preferences + const [autoSend, setAutoSend] = useState({ sendWeekly: false, sendMonthly: false, sendYearly: false }); + const [autoSendDirty, setAutoSendDirty] = useState(false); + const [savingAutoSend, setSavingAutoSend] = useState(false); + const [autoSendSnackbar, setAutoSendSnackbar] = useState(null); + + useEffect(() => { + axiosConfig.get('/GetEmailPreference', { params: { installationId } }) + .then(res => { + setAutoSend({ sendWeekly: res.data.sendWeekly, sendMonthly: res.data.sendMonthly, sendYearly: res.data.sendYearly }); + setAutoSendDirty(false); + }) + .catch(() => {}); + }, [installationId]); + + const handleAutoSendChange = (field: 'sendWeekly' | 'sendMonthly' | 'sendYearly') => { + setAutoSend(prev => ({ ...prev, [field]: !prev[field] })); + setAutoSendDirty(true); + }; + + const handleSaveAutoSend = async () => { + setSavingAutoSend(true); + try { + await axiosConfig.post('/UpdateEmailPreference', null, { + params: { installationId, ...autoSend } + }); + setAutoSendDirty(false); + setAutoSendSnackbar(intl.formatMessage({ id: 'autoSendSaved', defaultMessage: 'Auto-send preferences saved.' })); + } catch { + setAutoSendSnackbar(intl.formatMessage({ id: 'autoSendSaveFailed', defaultMessage: 'Failed to save auto-send preferences.' })); + } finally { + setSavingAutoSend(false); + } + }; + + const hasEmail = !!(installationEmail && installationEmail.trim()); + const fetchReportData = () => { const lang = intl.locale; axiosConfig.get('/GetMonthlyReports', { params: { installationId, language: lang } }) @@ -382,36 +423,60 @@ function WeeklyReport({ installationId, installationName }: WeeklyReportProps) { )} - {tabs[safeTab]?.key !== 'daily' && activeTabHasData && ( - + + + + + + + + + handleAutoSendChange('sendWeekly')} disabled={!hasEmail} size="small" />} + label={intl.formatMessage({ id: 'weeklyTab', defaultMessage: 'Weekly' })} + /> + + + + + handleAutoSendChange('sendMonthly')} disabled={!hasEmail} size="small" />} + label={intl.formatMessage({ id: 'monthlyTab', defaultMessage: 'Monthly' })} + /> + + + + + handleAutoSendChange('sendYearly')} disabled={!hasEmail} size="small" />} + label={intl.formatMessage({ id: 'yearlyTab', defaultMessage: 'Yearly' })} + /> + + + + {!hasEmail && ( + + + )} + setAutoSendSnackbar(null)} + message={autoSendSnackbar} + /> + setReportPeriod({ start: date, end: date })} /> diff --git a/typescript/frontend-marios2/src/interfaces/InstallationTypes.tsx b/typescript/frontend-marios2/src/interfaces/InstallationTypes.tsx index 95f63a9e4..1d91e5680 100644 --- a/typescript/frontend-marios2/src/interfaces/InstallationTypes.tsx +++ b/typescript/frontend-marios2/src/interfaces/InstallationTypes.tsx @@ -39,6 +39,7 @@ export interface I_Installation extends I_S3Credentials { status?: number; serialNumber?: string; networkProvider: string; + email: string; } export interface I_Folder { diff --git a/typescript/frontend-marios2/src/lang/de.json b/typescript/frontend-marios2/src/lang/de.json index 73af78ace..2593e1e4b 100644 --- a/typescript/frontend-marios2/src/lang/de.json +++ b/typescript/frontend-marios2/src/lang/de.json @@ -14,6 +14,7 @@ "inverterFirmwareVersion": "Wechselrichter-Firmware-Version", "batteryFirmwareVersion": "Batterie-Firmware-Version", "networkProvider": "Netzbetreiber", + "emailAddress": "E-Mail-Adresse", "createNewFolder": "Neuer Ordner", "createNewUser": "Neuer Benutzer", "customerName": "Kundenname", @@ -202,6 +203,10 @@ "generateMonth": "{month} {year} generieren ({count} Wochen)", "generateYear": "{year} generieren ({count} Monate)", "regenerateReport": "Neu generieren", + "autoSendReports": "Berichte automatisch senden:", + "autoSendSaved": "Automatische Versandeinstellungen gespeichert.", + "autoSendSaveFailed": "Fehler beim Speichern der automatischen Versandeinstellungen.", + "autoSendNoEmail": "E-Mail-Adresse im Reiter Information eingeben, um den automatischen Versand zu aktivieren", "generatingMonthly": "Wird generiert...", "generatingYearly": "Wird generiert...", "thisMonthWeeklyReports": "Wöchentliche Berichte dieses Monats", diff --git a/typescript/frontend-marios2/src/lang/en.json b/typescript/frontend-marios2/src/lang/en.json index b98a2615c..465ba0a15 100644 --- a/typescript/frontend-marios2/src/lang/en.json +++ b/typescript/frontend-marios2/src/lang/en.json @@ -10,6 +10,7 @@ "inverterFirmwareVersion": "Inverter Firmware Version", "batteryFirmwareVersion": "Battery Firmware Version", "networkProvider": "Network Provider", + "emailAddress": "Email Address", "customerName": "Customer name", "english": "English", "german": "German", @@ -184,6 +185,10 @@ "generateMonth": "Generate {month} {year} ({count} weeks)", "generateYear": "Generate {year} ({count} months)", "regenerateReport": "Regenerate", + "autoSendReports": "Auto-send reports:", + "autoSendSaved": "Auto-send preferences saved.", + "autoSendSaveFailed": "Failed to save auto-send preferences.", + "autoSendNoEmail": "Set email address in Information tab to enable auto-send", "generatingMonthly": "Generating...", "generatingYearly": "Generating...", "thisMonthWeeklyReports": "This Month's Weekly Reports", diff --git a/typescript/frontend-marios2/src/lang/fr.json b/typescript/frontend-marios2/src/lang/fr.json index e68c18761..bb963cb74 100644 --- a/typescript/frontend-marios2/src/lang/fr.json +++ b/typescript/frontend-marios2/src/lang/fr.json @@ -12,6 +12,7 @@ "inverterFirmwareVersion": "Version firmware onduleur", "batteryFirmwareVersion": "Version firmware batterie", "networkProvider": "Gestionnaire de réseau", + "emailAddress": "Adresse e-mail", "createNewFolder": "Nouveau dossier", "createNewUser": "Nouvel utilisateur", "customerName": "Nom du client", @@ -196,6 +197,10 @@ "generateMonth": "Générer {month} {year} ({count} semaines)", "generateYear": "Générer {year} ({count} mois)", "regenerateReport": "Régénérer", + "autoSendReports": "Envoi automatique des rapports :", + "autoSendSaved": "Préférences d'envoi automatique enregistrées.", + "autoSendSaveFailed": "Échec de l'enregistrement des préférences d'envoi automatique.", + "autoSendNoEmail": "Définir l'adresse e-mail dans l'onglet Information pour activer l'envoi automatique", "generatingMonthly": "Génération en cours...", "generatingYearly": "Génération en cours...", "thisMonthWeeklyReports": "Rapports hebdomadaires de ce mois", diff --git a/typescript/frontend-marios2/src/lang/it.json b/typescript/frontend-marios2/src/lang/it.json index 9cbbc233c..1aec72b4d 100644 --- a/typescript/frontend-marios2/src/lang/it.json +++ b/typescript/frontend-marios2/src/lang/it.json @@ -10,6 +10,7 @@ "inverterFirmwareVersion": "Versione firmware inverter", "batteryFirmwareVersion": "Versione firmware batteria", "networkProvider": "Gestore di rete", + "emailAddress": "Indirizzo e-mail", "customerName": "Nome cliente", "english": "Inglese", "german": "Tedesco", @@ -207,6 +208,10 @@ "generateMonth": "Genera {month} {year} ({count} settimane)", "generateYear": "Genera {year} ({count} mesi)", "regenerateReport": "Rigenera", + "autoSendReports": "Invio automatico rapporti:", + "autoSendSaved": "Preferenze di invio automatico salvate.", + "autoSendSaveFailed": "Impossibile salvare le preferenze di invio automatico.", + "autoSendNoEmail": "Impostare l'indirizzo e-mail nella scheda Informazioni per attivare l'invio automatico", "generatingMonthly": "Generazione in corso...", "generatingYearly": "Generazione in corso...", "thisMonthWeeklyReports": "Rapporti settimanali di questo mese",