allow more flexible email frequency

This commit is contained in:
Yinyin Liu 2026-03-28 17:19:22 +01:00
parent e989db54c2
commit b0bcf06d4e
15 changed files with 276 additions and 30 deletions

View File

@ -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 ──────────────────────────────────────
/// <summary>

View File

@ -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; }
}

View File

@ -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; } = "";
}

View File

@ -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);

View File

@ -31,6 +31,7 @@ public static partial class Db
public static TableQuery<DailyEnergyRecord> DailyRecords => Connection.Table<DailyEnergyRecord>();
public static TableQuery<HourlyEnergyRecord> HourlyRecords => Connection.Table<HourlyEnergyRecord>();
public static TableQuery<AiInsightCache> AiInsightCaches => Connection.Table<AiInsightCache>();
public static TableQuery<EmailPreference> EmailPreferences => Connection.Table<EmailPreference>();
// Ticket system tables
public static TableQuery<Ticket> Tickets => Connection.Table<Ticket>();
@ -69,6 +70,7 @@ public static partial class Db
Connection.CreateTable<DailyEnergyRecord>();
Connection.CreateTable<HourlyEnergyRecord>();
Connection.CreateTable<AiInsightCache>();
Connection.CreateTable<EmailPreference>();
// Ticket system tables
Connection.CreateTable<Ticket>();
@ -125,6 +127,7 @@ public static partial class Db
fileConnection.CreateTable<DailyEnergyRecord>();
fileConnection.CreateTable<HourlyEnergyRecord>();
fileConnection.CreateTable<AiInsightCache>();
fileConnection.CreateTable<EmailPreference>();
// Ticket system tables
fileConnection.CreateTable<Ticket>();

View File

@ -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)

View File

@ -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 ──────────────────────────────────────────────

View File

@ -708,6 +708,18 @@ function InformationSodistorehome(props: InformationSodistorehomeProps) {
/>
</div>
<div>
<TextField
label={<FormattedMessage id="emailAddress" defaultMessage="Email Address" />}
name="email"
value={formValues.email || ''}
onChange={handleChange}
variant="outlined"
fullWidth
inputProps={{ readOnly: !canEdit }}
/>
</div>
<div>
<FormControl fullWidth sx={{ marginLeft: 1, marginTop: 1, marginBottom: 1, width: 440 }}>
<InputLabel sx={{ fontSize: 14, backgroundColor: 'white' }}>

View File

@ -619,6 +619,7 @@ function SodioHomeInstallation(props: singleInstallationProps) {
<WeeklyReport
installationId={props.current_installation.id}
installationName={props.current_installation.name}
installationEmail={props.current_installation.email}
/>
}
/>

View File

@ -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<voi
// ── Main Component ─────────────────────────────────────────────
function WeeklyReport({ installationId, installationName }: WeeklyReportProps) {
function WeeklyReport({ installationId, installationName, installationEmail }: WeeklyReportProps) {
const intl = useIntl();
const [activeTab, setActiveTab] = useState(0);
const [monthlyReports, setMonthlyReports] = useState<MonthlyReport[]>([]);
@ -234,13 +239,49 @@ function WeeklyReport({ installationId, installationName }: WeeklyReportProps) {
const [generating, setGenerating] = useState<string | null>(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<WeeklySectionHandle>(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<string | null>(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) {
<FormattedMessage id="downloadPdf" defaultMessage="Download PDF" />
</Button>
)}
{tabs[safeTab]?.key !== 'daily' && activeTabHasData && (
<Button
variant="outlined"
disabled={regenerating || generating !== null}
startIcon={(regenerating || generating !== null) ? <CircularProgress size={16} /> : <RefreshIcon />}
onClick={async () => {
const key = tabs[safeTab]?.key;
if (key === 'weekly') {
weeklyRef.current?.regenerate();
} else if (key === 'monthly') {
const r = monthlyReports[selectedMonthlyIdx];
if (r) {
setRegenerating(true);
try { await handleGenerateMonthly(r.year, r.month); } finally { setRegenerating(false); }
}
} else if (key === 'yearly') {
const r = yearlyReports[selectedYearlyIdx];
if (r) {
setRegenerating(true);
try { await handleGenerateYearly(r.year); } finally { setRegenerating(false); }
}
}
}}
sx={{ ml: 1, whiteSpace: 'nowrap' }}
>
<FormattedMessage id="regenerateReport" defaultMessage="Regenerate" />
</Button>
</Box>
<Box sx={{ display: 'flex', alignItems: 'center', mb: 2, gap: 1, flexWrap: 'wrap' }} className="no-print">
<Typography variant="body2" sx={{ fontWeight: 'bold', color: '#555' }}>
<FormattedMessage id="autoSendReports" defaultMessage="Auto-send reports:" />
</Typography>
<Tooltip title={!hasEmail ? intl.formatMessage({ id: 'autoSendNoEmail', defaultMessage: 'Set email address in Information tab to enable auto-send' }) : ''}>
<span>
<FormControlLabel
control={<Checkbox checked={autoSend.sendWeekly} onChange={() => handleAutoSendChange('sendWeekly')} disabled={!hasEmail} size="small" />}
label={intl.formatMessage({ id: 'weeklyTab', defaultMessage: 'Weekly' })}
/>
</span>
</Tooltip>
<Tooltip title={!hasEmail ? intl.formatMessage({ id: 'autoSendNoEmail', defaultMessage: 'Set email address in Information tab to enable auto-send' }) : ''}>
<span>
<FormControlLabel
control={<Checkbox checked={autoSend.sendMonthly} onChange={() => handleAutoSendChange('sendMonthly')} disabled={!hasEmail} size="small" />}
label={intl.formatMessage({ id: 'monthlyTab', defaultMessage: 'Monthly' })}
/>
</span>
</Tooltip>
<Tooltip title={!hasEmail ? intl.formatMessage({ id: 'autoSendNoEmail', defaultMessage: 'Set email address in Information tab to enable auto-send' }) : ''}>
<span>
<FormControlLabel
control={<Checkbox checked={autoSend.sendYearly} onChange={() => handleAutoSendChange('sendYearly')} disabled={!hasEmail} size="small" />}
label={intl.formatMessage({ id: 'yearlyTab', defaultMessage: 'Yearly' })}
/>
</span>
</Tooltip>
<Button
variant="contained"
size="small"
startIcon={savingAutoSend ? <CircularProgress size={14} /> : <SaveIcon />}
onClick={handleSaveAutoSend}
disabled={!autoSendDirty || savingAutoSend}
sx={{ ml: 1 }}
>
<FormattedMessage id="applyChanges" defaultMessage="Save" />
</Button>
{!hasEmail && (
<Typography variant="caption" sx={{ color: '#e67e22', ml: 1 }}>
<FormattedMessage id="autoSendNoEmail" defaultMessage="Set email address in Information tab to enable auto-send" />
</Typography>
)}
</Box>
<Snackbar
open={!!autoSendSnackbar}
autoHideDuration={4000}
onClose={() => setAutoSendSnackbar(null)}
message={autoSendSnackbar}
/>
<Box sx={{ display: tabs[safeTab]?.key === 'daily' ? 'block' : 'none', minHeight: '50vh' }}>
<DailySection installationId={installationId} onHasData={setDailyHasData} onPeriodChange={(date: string) => setReportPeriod({ start: date, end: date })} />
</Box>

View File

@ -39,6 +39,7 @@ export interface I_Installation extends I_S3Credentials {
status?: number;
serialNumber?: string;
networkProvider: string;
email: string;
}
export interface I_Folder {

View File

@ -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",

View File

@ -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",

View File

@ -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",

View File

@ -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",