added language support for monitor AI and non-AI content and email delivery
This commit is contained in:
parent
77f6e0de6c
commit
8e50220242
|
|
@ -763,7 +763,7 @@ public class Controller : ControllerBase
|
||||||
installation.Product != (int)ProductType.SodiStoreMax)
|
installation.Product != (int)ProductType.SodiStoreMax)
|
||||||
return BadRequest("AI diagnostics not available for this product.");
|
return BadRequest("AI diagnostics not available for this product.");
|
||||||
|
|
||||||
var result = await DiagnosticService.DiagnoseAsync(installationId, errorDescription);
|
var result = await DiagnosticService.DiagnoseAsync(installationId, errorDescription, user.Language ?? "en");
|
||||||
|
|
||||||
if (result is null)
|
if (result is null)
|
||||||
return NoContent(); // no diagnosis available (not in knowledge base, no API key)
|
return NoContent(); // no diagnosis available (not in knowledge base, no API key)
|
||||||
|
|
@ -869,7 +869,7 @@ public class Controller : ControllerBase
|
||||||
/// Returns JSON with daily data, weekly totals, ratios, and AI insight.
|
/// Returns JSON with daily data, weekly totals, ratios, and AI insight.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[HttpGet(nameof(GetWeeklyReport))]
|
[HttpGet(nameof(GetWeeklyReport))]
|
||||||
public async Task<ActionResult<WeeklyReportResponse>> GetWeeklyReport(Int64 installationId, Token authToken)
|
public async Task<ActionResult<WeeklyReportResponse>> GetWeeklyReport(Int64 installationId, Token authToken, string? language = null)
|
||||||
{
|
{
|
||||||
var user = Db.GetSession(authToken)?.User;
|
var user = Db.GetSession(authToken)?.User;
|
||||||
if (user == null)
|
if (user == null)
|
||||||
|
|
@ -885,7 +885,8 @@ public class Controller : ControllerBase
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var report = await WeeklyReportService.GenerateReportAsync(installationId, installation.InstallationName);
|
var lang = language ?? user.Language ?? "en";
|
||||||
|
var report = await WeeklyReportService.GenerateReportAsync(installationId, installation.InstallationName, lang);
|
||||||
return Ok(report);
|
return Ok(report);
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
|
|
@ -911,8 +912,9 @@ public class Controller : ControllerBase
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var report = await WeeklyReportService.GenerateReportAsync(installationId, installation.InstallationName);
|
var lang = user.Language ?? "en";
|
||||||
await ReportEmailService.SendReportEmailAsync(report, emailAddress);
|
var report = await WeeklyReportService.GenerateReportAsync(installationId, installation.InstallationName, lang);
|
||||||
|
await ReportEmailService.SendReportEmailAsync(report, emailAddress, lang);
|
||||||
return Ok(new { message = $"Report sent to {emailAddress}" });
|
return Ok(new { message = $"Report sent to {emailAddress}" });
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
|
|
|
||||||
|
|
@ -237,26 +237,63 @@ public static class UserMethods
|
||||||
|
|
||||||
public static Task SendPasswordResetEmail(this User user, String token)
|
public static Task SendPasswordResetEmail(this User user, String token)
|
||||||
{
|
{
|
||||||
const String subject = "Reset the password of your Inesco Energy Account";
|
|
||||||
const String resetLink = "https://monitor.inesco.energy/api/ResetPassword"; // TODO: move to settings file
|
const String resetLink = "https://monitor.inesco.energy/api/ResetPassword"; // TODO: move to settings file
|
||||||
var encodedToken = HttpUtility.UrlEncode(token);
|
var encodedToken = HttpUtility.UrlEncode(token);
|
||||||
|
|
||||||
var body = $"Dear {user.Name}\n" +
|
var (subject, body) = (user.Language ?? "en") switch
|
||||||
$"To reset your password " +
|
{
|
||||||
$"please open this link:{resetLink}?token={encodedToken}";
|
"de" => (
|
||||||
|
"Passwort Ihres Inesco Energy Kontos zurücksetzen",
|
||||||
|
$"Sehr geehrte/r {user.Name}\n" +
|
||||||
|
$"Um Ihr Passwort zurückzusetzen, öffnen Sie bitte diesen Link: {resetLink}?token={encodedToken}"
|
||||||
|
),
|
||||||
|
"fr" => (
|
||||||
|
"Réinitialisation du mot de passe de votre compte Inesco Energy",
|
||||||
|
$"Cher/Chère {user.Name}\n" +
|
||||||
|
$"Pour réinitialiser votre mot de passe, veuillez ouvrir ce lien : {resetLink}?token={encodedToken}"
|
||||||
|
),
|
||||||
|
"it" => (
|
||||||
|
"Reimposta la password del tuo account Inesco Energy",
|
||||||
|
$"Gentile {user.Name}\n" +
|
||||||
|
$"Per reimpostare la password, apra questo link: {resetLink}?token={encodedToken}"
|
||||||
|
),
|
||||||
|
_ => (
|
||||||
|
"Reset the password of your Inesco Energy Account",
|
||||||
|
$"Dear {user.Name}\n" +
|
||||||
|
$"To reset your password please open this link: {resetLink}?token={encodedToken}"
|
||||||
|
)
|
||||||
|
};
|
||||||
|
|
||||||
return user.SendEmail(subject, body);
|
return user.SendEmail(subject, body);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static Task SendNewUserWelcomeMessage(this User user)
|
public static Task SendNewUserWelcomeMessage(this User user)
|
||||||
{
|
{
|
||||||
const String subject = "Your new Inesco Energy Account";
|
|
||||||
|
|
||||||
var resetLink = $"https://monitor.inesco.energy/?username={user.Email}"; // TODO: move to settings file
|
var resetLink = $"https://monitor.inesco.energy/?username={user.Email}"; // TODO: move to settings file
|
||||||
|
|
||||||
var body = $"Dear {user.Name}\n" +
|
var (subject, body) = (user.Language ?? "en") switch
|
||||||
$"To set your password and log in to your " +
|
{
|
||||||
$"Inesco Energy Account open this link:{resetLink}";
|
"de" => (
|
||||||
|
"Ihr neues Inesco Energy Konto",
|
||||||
|
$"Sehr geehrte/r {user.Name}\n" +
|
||||||
|
$"Um Ihr Passwort festzulegen und sich bei Ihrem Inesco Energy Konto anzumelden, öffnen Sie bitte diesen Link: {resetLink}"
|
||||||
|
),
|
||||||
|
"fr" => (
|
||||||
|
"Votre nouveau compte Inesco Energy",
|
||||||
|
$"Cher/Chère {user.Name}\n" +
|
||||||
|
$"Pour définir votre mot de passe et vous connecter à votre compte Inesco Energy, veuillez ouvrir ce lien : {resetLink}"
|
||||||
|
),
|
||||||
|
"it" => (
|
||||||
|
"Il tuo nuovo account Inesco Energy",
|
||||||
|
$"Gentile {user.Name}\n" +
|
||||||
|
$"Per impostare la password e accedere al suo account Inesco Energy, apra questo link: {resetLink}"
|
||||||
|
),
|
||||||
|
_ => (
|
||||||
|
"Your new Inesco Energy Account",
|
||||||
|
$"Dear {user.Name}\n" +
|
||||||
|
$"To set your password and log in to your Inesco Energy Account open this link: {resetLink}"
|
||||||
|
)
|
||||||
|
};
|
||||||
|
|
||||||
return user.SendEmail(subject, body);
|
return user.SendEmail(subject, body);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@ public class User : TreeNode
|
||||||
public int UserType { get; set; } = 0;
|
public int UserType { get; set; } = 0;
|
||||||
public Boolean MustResetPassword { get; set; } = false;
|
public Boolean MustResetPassword { get; set; } = false;
|
||||||
public String? Password { get; set; } = null!;
|
public String? Password { get; set; } = null!;
|
||||||
|
public String Language { get; set; } = "en";
|
||||||
|
|
||||||
[Unique]
|
[Unique]
|
||||||
public override String Name { get; set; } = null!;
|
public override String Name { get; set; } = null!;
|
||||||
|
|
|
||||||
|
|
@ -53,6 +53,12 @@ public static partial class Db
|
||||||
Connection.CreateTable<UserAction>();
|
Connection.CreateTable<UserAction>();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// One-time migration: normalize legacy long-form language values to ISO codes
|
||||||
|
Connection.Execute("UPDATE User SET Language = 'en' WHERE Language IS NULL OR Language = '' OR Language = 'english'");
|
||||||
|
Connection.Execute("UPDATE User SET Language = 'de' WHERE Language = 'german'");
|
||||||
|
Connection.Execute("UPDATE User SET Language = 'fr' WHERE Language = 'french'");
|
||||||
|
Connection.Execute("UPDATE User SET Language = 'it' WHERE Language = 'italian'");
|
||||||
|
|
||||||
//UpdateKeys();
|
//UpdateKeys();
|
||||||
CleanupSessions().SupressAwaitWarning();
|
CleanupSessions().SupressAwaitWarning();
|
||||||
DeleteSnapshots().SupressAwaitWarning();
|
DeleteSnapshots().SupressAwaitWarning();
|
||||||
|
|
|
||||||
|
|
@ -38,26 +38,40 @@ public static class DiagnosticService
|
||||||
|
|
||||||
// ── public entry-point ──────────────────────────────────────────
|
// ── public entry-point ──────────────────────────────────────────
|
||||||
|
|
||||||
/// <summary>
|
private static string LanguageName(string code) => code switch
|
||||||
/// Returns a diagnosis for <paramref name="errorDescription"/>.
|
{
|
||||||
/// First checks the static AlarmKnowledgeBase for known Sinexcel/Growatt alarms.
|
"de" => "German",
|
||||||
/// Falls back to in-memory cache, then calls Mistral AI only for unknown alarms.
|
"fr" => "French",
|
||||||
/// </summary>
|
"it" => "Italian",
|
||||||
public static async Task<DiagnosticResponse?> DiagnoseAsync(Int64 installationId, string errorDescription)
|
_ => "English"
|
||||||
|
};
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Returns a diagnosis for <paramref name="errorDescription"/> in the given language.
|
||||||
|
/// For English: checks the static AlarmKnowledgeBase first, then in-memory cache, then Mistral AI.
|
||||||
|
/// For other languages: skips the knowledge base (English-only) and goes directly to Mistral AI.
|
||||||
|
/// Cache is keyed by (errorDescription, language) so each language is cached separately.
|
||||||
|
/// </summary>
|
||||||
|
public static async Task<DiagnosticResponse?> DiagnoseAsync(Int64 installationId, string errorDescription, string language = "en")
|
||||||
|
{
|
||||||
|
var cacheKey = $"{errorDescription}|{language}";
|
||||||
|
|
||||||
|
// 1. For English only: check the static knowledge base first (no API call needed)
|
||||||
|
if (language == "en")
|
||||||
{
|
{
|
||||||
// 1. Check the static knowledge base first (no API call needed)
|
|
||||||
var knownDiagnosis = AlarmKnowledgeBase.TryGetDiagnosis(errorDescription);
|
var knownDiagnosis = AlarmKnowledgeBase.TryGetDiagnosis(errorDescription);
|
||||||
if (knownDiagnosis is not null)
|
if (knownDiagnosis is not null)
|
||||||
{
|
{
|
||||||
Console.WriteLine($"[DiagnosticService] Found diagnosis in knowledge base for: {errorDescription}");
|
Console.WriteLine($"[DiagnosticService] Found diagnosis in knowledge base for: {errorDescription}");
|
||||||
return knownDiagnosis;
|
return knownDiagnosis;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 2. If AI is not enabled, we can't proceed further
|
// 2. If AI is not enabled, we can't proceed further
|
||||||
if (!IsEnabled) return null;
|
if (!IsEnabled) return null;
|
||||||
|
|
||||||
// 3. Check in-memory cache for previously fetched AI diagnoses
|
// 3. Check in-memory cache for previously fetched AI diagnoses
|
||||||
if (Cache.TryGetValue(errorDescription, out var cached))
|
if (Cache.TryGetValue(cacheKey, out var cached))
|
||||||
return cached;
|
return cached;
|
||||||
|
|
||||||
// 4. Gather context from the DB for AI prompt
|
// 4. Gather context from the DB for AI prompt
|
||||||
|
|
@ -77,14 +91,14 @@ public static class DiagnosticService
|
||||||
.ToList();
|
.ToList();
|
||||||
|
|
||||||
// 5. Build prompt and call Mistral API (only for unknown alarms)
|
// 5. Build prompt and call Mistral API (only for unknown alarms)
|
||||||
Console.WriteLine($"[DiagnosticService] Calling Mistral for unknown alarm: {errorDescription}");
|
Console.WriteLine($"[DiagnosticService] Calling Mistral for unknown alarm: {errorDescription} ({language})");
|
||||||
var prompt = BuildPrompt(errorDescription, productName, recentDescriptions);
|
var prompt = BuildPrompt(errorDescription, productName, recentDescriptions, language);
|
||||||
var response = await CallMistralAsync(prompt);
|
var response = await CallMistralAsync(prompt);
|
||||||
|
|
||||||
if (response is null) return null;
|
if (response is null) return null;
|
||||||
|
|
||||||
// 6. Store in cache for future requests
|
// 6. Store in cache for future requests
|
||||||
Cache.TryAdd(errorDescription, response);
|
Cache.TryAdd(cacheKey, response);
|
||||||
return response;
|
return response;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -101,7 +115,7 @@ public static class DiagnosticService
|
||||||
if (Cache.TryGetValue(errorDescription, out var cached))
|
if (Cache.TryGetValue(errorDescription, out var cached))
|
||||||
return cached;
|
return cached;
|
||||||
|
|
||||||
var prompt = BuildPrompt(errorDescription, "SodioHome", new List<string>());
|
var prompt = BuildPrompt(errorDescription, "SodioHome", new List<string>(), "en");
|
||||||
var response = await CallMistralAsync(prompt);
|
var response = await CallMistralAsync(prompt);
|
||||||
|
|
||||||
if (response is not null)
|
if (response is not null)
|
||||||
|
|
@ -112,7 +126,7 @@ public static class DiagnosticService
|
||||||
|
|
||||||
// ── prompt ──────────────────────────────────────────────────────
|
// ── prompt ──────────────────────────────────────────────────────
|
||||||
|
|
||||||
private static string BuildPrompt(string errorDescription, string productName, List<string> recentErrors)
|
private static string BuildPrompt(string errorDescription, string productName, List<string> recentErrors, string language = "en")
|
||||||
{
|
{
|
||||||
var recentList = recentErrors.Count > 0
|
var recentList = recentErrors.Count > 0
|
||||||
? string.Join(", ", recentErrors)
|
? string.Join(", ", recentErrors)
|
||||||
|
|
@ -128,7 +142,7 @@ Explain for a non-technical homeowner. Keep it very short and simple:
|
||||||
- explanation: 1 short sentence, no jargon
|
- explanation: 1 short sentence, no jargon
|
||||||
- causes: 2-3 bullet points, plain language
|
- causes: 2-3 bullet points, plain language
|
||||||
- nextSteps: 2-3 simple action items a homeowner can understand
|
- nextSteps: 2-3 simple action items a homeowner can understand
|
||||||
Reply with ONLY valid JSON, no markdown:
|
IMPORTANT: Write all text values in {LanguageName(language)}. Reply with ONLY valid JSON, no markdown:
|
||||||
{{""explanation"":""1 short sentence"",""causes"":[""...""],""nextSteps"":[""...""]}}
|
{{""explanation"":""1 short sentence"",""causes"":[""...""],""nextSteps"":[""...""]}}
|
||||||
";
|
";
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -10,13 +10,14 @@ namespace InnovEnergy.App.Backend.Services;
|
||||||
public static class ReportEmailService
|
public static class ReportEmailService
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Sends the weekly report as a nicely formatted HTML email.
|
/// 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.
|
/// Uses MailKit directly (same config as existing Mailer library) but with HTML support.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static async Task SendReportEmailAsync(WeeklyReportResponse report, string recipientEmail)
|
public static async Task SendReportEmailAsync(WeeklyReportResponse report, string recipientEmail, string language = "en")
|
||||||
{
|
{
|
||||||
var subject = $"Weekly Energy Report — {report.InstallationName} ({report.PeriodStart} to {report.PeriodEnd})";
|
var strings = GetStrings(language);
|
||||||
var html = BuildHtmlEmail(report);
|
var subject = $"{strings.Title} — {report.InstallationName} ({report.PeriodStart} to {report.PeriodEnd})";
|
||||||
|
var html = BuildHtmlEmail(report, strings);
|
||||||
|
|
||||||
var config = await ReadMailerConfig();
|
var config = await ReadMailerConfig();
|
||||||
|
|
||||||
|
|
@ -49,9 +50,169 @@ public static class ReportEmailService
|
||||||
return config ?? throw new InvalidOperationException("Failed to read MailerConfig.json");
|
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
|
||||||
|
);
|
||||||
|
|
||||||
|
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 Ein/Aus",
|
||||||
|
SolarEnergyUsed: "Genutzte Solarenergie",
|
||||||
|
StayedAtHome: "direkt genutzt",
|
||||||
|
EstMoneySaved: "Geschätzte Ersparnis",
|
||||||
|
AtRate: "bei 0.27 CHF/kWh",
|
||||||
|
SolarCoverage: "Solare Deckung",
|
||||||
|
FromSolar: "durch Solar",
|
||||||
|
BatteryEff: "Batterie-Eff.",
|
||||||
|
OutVsIn: "Aus vs. Ein",
|
||||||
|
Day: "Tag",
|
||||||
|
Load: "Last",
|
||||||
|
GridIn: "Netz Ein",
|
||||||
|
GridOut: "Netz Aus",
|
||||||
|
BattInOut: "Batt. Ein/Aus",
|
||||||
|
Footer: "Erstellt von <strong style=\"color:#666\">Inesco Energy Monitor Platform</strong> · Powered by Mistral AI"
|
||||||
|
),
|
||||||
|
"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 Entrée/Sortie",
|
||||||
|
SolarEnergyUsed: "Énergie solaire utilisée",
|
||||||
|
StayedAtHome: "autoconsommée",
|
||||||
|
EstMoneySaved: "Économies estimées",
|
||||||
|
AtRate: "à 0.27 CHF/kWh",
|
||||||
|
SolarCoverage: "Couverture solaire",
|
||||||
|
FromSolar: "depuis le solaire",
|
||||||
|
BatteryEff: "Eff. batterie",
|
||||||
|
OutVsIn: "sortie vs entrée",
|
||||||
|
Day: "Jour",
|
||||||
|
Load: "Charge",
|
||||||
|
GridIn: "Réseau Ent.",
|
||||||
|
GridOut: "Réseau Sor.",
|
||||||
|
BattInOut: "Batt. Ent./Sor.",
|
||||||
|
Footer: "Généré par <strong style=\"color:#666\">Inesco Energy Monitor Platform</strong> · Propulsé par Mistral AI"
|
||||||
|
),
|
||||||
|
"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 Ent./Usc.",
|
||||||
|
SolarEnergyUsed: "Energia solare utilizzata",
|
||||||
|
StayedAtHome: "rimasta in casa",
|
||||||
|
EstMoneySaved: "Risparmio stimato",
|
||||||
|
AtRate: "a 0.27 CHF/kWh",
|
||||||
|
SolarCoverage: "Copertura solare",
|
||||||
|
FromSolar: "dal solare",
|
||||||
|
BatteryEff: "Eff. batteria",
|
||||||
|
OutVsIn: "uscita vs entrata",
|
||||||
|
Day: "Giorno",
|
||||||
|
Load: "Carico",
|
||||||
|
GridIn: "Rete Ent.",
|
||||||
|
GridOut: "Rete Usc.",
|
||||||
|
BattInOut: "Batt. Ent./Usc.",
|
||||||
|
Footer: "Generato da <strong style=\"color:#666\">Inesco Energy Monitor Platform</strong> · Powered by Mistral AI"
|
||||||
|
),
|
||||||
|
_ => 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 In/Out",
|
||||||
|
SolarEnergyUsed: "Solar Energy Used",
|
||||||
|
StayedAtHome: "stayed at home",
|
||||||
|
EstMoneySaved: "Est. Money Saved",
|
||||||
|
AtRate: "at 0.27 CHF/kWh",
|
||||||
|
SolarCoverage: "Solar Coverage",
|
||||||
|
FromSolar: "from solar",
|
||||||
|
BatteryEff: "Battery Eff.",
|
||||||
|
OutVsIn: "out vs in",
|
||||||
|
Day: "Day",
|
||||||
|
Load: "Load",
|
||||||
|
GridIn: "Grid In",
|
||||||
|
GridOut: "Grid Out",
|
||||||
|
BattInOut: "Batt In/Out",
|
||||||
|
Footer: "Generated by <strong style=\"color:#666\">Inesco Energy Monitor Platform</strong> · Powered by Mistral AI"
|
||||||
|
)
|
||||||
|
};
|
||||||
|
|
||||||
// ── HTML email template ─────────────────────────────────────────────
|
// ── HTML email template ─────────────────────────────────────────────
|
||||||
|
|
||||||
public static string BuildHtmlEmail(WeeklyReportResponse r)
|
public static string BuildHtmlEmail(WeeklyReportResponse r, string language = "en")
|
||||||
|
=> BuildHtmlEmail(r, GetStrings(language));
|
||||||
|
|
||||||
|
private static string BuildHtmlEmail(WeeklyReportResponse r, EmailStrings s)
|
||||||
{
|
{
|
||||||
var cur = r.CurrentWeek;
|
var cur = r.CurrentWeek;
|
||||||
var prev = r.PreviousWeek;
|
var prev = r.PreviousWeek;
|
||||||
|
|
@ -91,47 +252,47 @@ public static class ReportEmailService
|
||||||
var comparisonHtml = prev != null
|
var comparisonHtml = prev != null
|
||||||
? $@"
|
? $@"
|
||||||
<tr>
|
<tr>
|
||||||
<td style=""padding:8px 12px;border-bottom:1px solid #eee"">PV Production</td>
|
<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;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:#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>
|
<td style=""padding:8px 12px;border-bottom:1px solid #eee;text-align:right;color:{ChangeColor(r.PvChangePercent)}"">{FormatChange(r.PvChangePercent)}</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td style=""padding:8px 12px;border-bottom:1px solid #eee"">Consumption</td>
|
<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;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:#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>
|
<td style=""padding:8px 12px;border-bottom:1px solid #eee;text-align:right;color:{ChangeColor(-r.ConsumptionChangePercent)}"">{FormatChange(r.ConsumptionChangePercent)}</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td style=""padding:8px 12px;border-bottom:1px solid #eee"">Grid Import</td>
|
<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;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:#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>
|
<td style=""padding:8px 12px;border-bottom:1px solid #eee;text-align:right;color:{ChangeColor(-r.GridImportChangePercent)}"">{FormatChange(r.GridImportChangePercent)}</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td style=""padding:8px 12px;border-bottom:1px solid #eee"">Grid Export</td>
|
<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;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;color:#888"">{prev.TotalGridExport:F1} kWh</td>
|
||||||
<td style=""padding:8px 12px;border-bottom:1px solid #eee;text-align:right"">—</td>
|
<td style=""padding:8px 12px;border-bottom:1px solid #eee;text-align:right"">—</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td style=""padding:8px 12px;border-bottom:1px solid #eee"">Battery In/Out</td>
|
<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;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;color:#888"">{prev.TotalBatteryCharged:F1}/{prev.TotalBatteryDischarged:F1} kWh</td>
|
||||||
<td style=""padding:8px 12px;border-bottom:1px solid #eee;text-align:right"">—</td>
|
<td style=""padding:8px 12px;border-bottom:1px solid #eee;text-align:right"">—</td>
|
||||||
</tr>"
|
</tr>"
|
||||||
: $@"
|
: $@"
|
||||||
<tr><td style=""padding:8px 12px"">PV Production</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.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"">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.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"">Grid Import</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.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"">Grid Export</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.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"">Battery In/Out</td><td style=""padding:8px 12px;text-align:right;font-weight:bold"">{cur.TotalBatteryCharged:F1}/{cur.TotalBatteryDischarged: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
|
var comparisonHeaders = prev != null
|
||||||
? @"<th style=""padding:8px 12px;text-align:right"">This Week</th>
|
? $@"<th style=""padding:8px 12px;text-align:right"">{s.ThisWeek}</th>
|
||||||
<th style=""padding:8px 12px;text-align:right"">Last Week</th>
|
<th style=""padding:8px 12px;text-align:right"">{s.LastWeek}</th>
|
||||||
<th style=""padding:8px 12px;text-align:right"">Change</th>"
|
<th style=""padding:8px 12px;text-align:right"">{s.Change}</th>"
|
||||||
: @"<th style=""padding:8px 12px;text-align:right"">This Week</th>";
|
: $@"<th style=""padding:8px 12px;text-align:right"">{s.ThisWeek}</th>";
|
||||||
|
|
||||||
return $@"
|
return $@"
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
|
|
@ -145,7 +306,7 @@ public static class ReportEmailService
|
||||||
<!-- Header -->
|
<!-- Header -->
|
||||||
<tr>
|
<tr>
|
||||||
<td style=""background:#2c3e50;padding:24px 30px;color:#ffffff"">
|
<td style=""background:#2c3e50;padding:24px 30px;color:#ffffff"">
|
||||||
<div style=""font-size:20px;font-weight:bold"">Weekly Performance Report</div>
|
<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: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>
|
<div style=""font-size:13px;margin-top:2px;opacity:0.7"">{r.PeriodStart} — {r.PeriodEnd}</div>
|
||||||
</td>
|
</td>
|
||||||
|
|
@ -154,7 +315,7 @@ public static class ReportEmailService
|
||||||
<!-- Weekly Insights (top) -->
|
<!-- Weekly Insights (top) -->
|
||||||
<tr>
|
<tr>
|
||||||
<td style=""padding:24px 30px 0"">
|
<td style=""padding:24px 30px 0"">
|
||||||
<div style=""font-size:16px;font-weight:bold;margin-bottom:12px;color:#2c3e50"">Weekly Insights</div>
|
<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"">
|
<div style=""background:#fef9e7;border-left:4px solid #f39c12;padding:14px 18px;border-radius:0 6px 6px 0;font-size:14px;color:#333"">
|
||||||
{insightHtml}
|
{insightHtml}
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -164,10 +325,10 @@ public static class ReportEmailService
|
||||||
<!-- Weekly Totals -->
|
<!-- Weekly Totals -->
|
||||||
<tr>
|
<tr>
|
||||||
<td style=""padding:24px 30px"">
|
<td style=""padding:24px 30px"">
|
||||||
<div style=""font-size:16px;font-weight:bold;margin-bottom:12px;color:#2c3e50"">Weekly Summary</div>
|
<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"">
|
<table width=""100%"" cellpadding=""0"" cellspacing=""0"" style=""border:1px solid #eee;border-radius:4px"">
|
||||||
<tr style=""background:#f8f9fa"">
|
<tr style=""background:#f8f9fa"">
|
||||||
<th style=""padding:8px 12px;text-align:left"">Metric</th>
|
<th style=""padding:8px 12px;text-align:left"">{s.Metric}</th>
|
||||||
{comparisonHeaders}
|
{comparisonHeaders}
|
||||||
</tr>
|
</tr>
|
||||||
{comparisonHtml}
|
{comparisonHtml}
|
||||||
|
|
@ -178,13 +339,13 @@ public static class ReportEmailService
|
||||||
<!-- Key Ratios -->
|
<!-- Key Ratios -->
|
||||||
<tr>
|
<tr>
|
||||||
<td style=""padding:0 30px 24px"">
|
<td style=""padding:0 30px 24px"">
|
||||||
<div style=""font-size:16px;font-weight:bold;margin-bottom:12px;color:#2c3e50"">Your Savings This Week</div>
|
<div style=""font-size:16px;font-weight:bold;margin-bottom:12px;color:#2c3e50"">{s.SavingsHeader}</div>
|
||||||
<table width=""100%"" cellpadding=""0"" cellspacing=""8"">
|
<table width=""100%"" cellpadding=""0"" cellspacing=""8"">
|
||||||
<tr>
|
<tr>
|
||||||
{SavingsBox("Solar Energy Used", $"{r.CurrentWeek.TotalPvProduction - r.CurrentWeek.TotalGridExport:F1} kWh", "stayed at home", "#27ae60")}
|
{SavingsBox(s.SolarEnergyUsed, $"{r.CurrentWeek.TotalPvProduction - r.CurrentWeek.TotalGridExport:F1} kWh", s.StayedAtHome, "#27ae60")}
|
||||||
{SavingsBox("Est. Money Saved", $"~{(r.CurrentWeek.TotalPvProduction - r.CurrentWeek.TotalGridExport) * 0.27:F1} CHF", "at 0.27 CHF/kWh", "#2980b9")}
|
{SavingsBox(s.EstMoneySaved, $"~{(r.CurrentWeek.TotalPvProduction - r.CurrentWeek.TotalGridExport) * 0.27:F1} CHF", s.AtRate, "#2980b9")}
|
||||||
{SavingsBox("Solar Coverage", $"{r.SelfSufficiencyPercent:F0}%", "from solar", "#8e44ad")}
|
{SavingsBox(s.SolarCoverage, $"{r.SelfSufficiencyPercent:F0}%", s.FromSolar, "#8e44ad")}
|
||||||
{SavingsBox("Battery Eff.", $"{r.BatteryEfficiencyPercent:F0}%", "out vs in", "#e67e22")}
|
{SavingsBox(s.BatteryEff, $"{r.BatteryEfficiencyPercent:F0}%", s.OutVsIn, "#e67e22")}
|
||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
</td>
|
</td>
|
||||||
|
|
@ -193,15 +354,15 @@ public static class ReportEmailService
|
||||||
<!-- Daily Breakdown -->
|
<!-- Daily Breakdown -->
|
||||||
<tr>
|
<tr>
|
||||||
<td style=""padding:0 30px 24px"">
|
<td style=""padding:0 30px 24px"">
|
||||||
<div style=""font-size:16px;font-weight:bold;margin-bottom:12px;color:#2c3e50"">Daily Breakdown (kWh)</div>
|
<div style=""font-size:16px;font-weight:bold;margin-bottom:12px;color:#2c3e50"">{s.DailyBreakdown}</div>
|
||||||
<table width=""100%"" cellpadding=""0"" cellspacing=""0"" style=""border:1px solid #eee;border-radius:4px;font-size:13px"">
|
<table width=""100%"" cellpadding=""0"" cellspacing=""0"" style=""border:1px solid #eee;border-radius:4px;font-size:13px"">
|
||||||
<tr style=""background:#f8f9fa"">
|
<tr style=""background:#f8f9fa"">
|
||||||
<th style=""padding:6px 10px;text-align:left"">Day</th>
|
<th style=""padding:6px 10px;text-align:left"">{s.Day}</th>
|
||||||
<th style=""padding:6px 10px;text-align:right"">PV</th>
|
<th style=""padding:6px 10px;text-align:right"">PV</th>
|
||||||
<th style=""padding:6px 10px;text-align:right"">Load</th>
|
<th style=""padding:6px 10px;text-align:right"">{s.Load}</th>
|
||||||
<th style=""padding:6px 10px;text-align:right"">Grid In</th>
|
<th style=""padding:6px 10px;text-align:right"">{s.GridIn}</th>
|
||||||
<th style=""padding:6px 10px;text-align:right"">Grid Out</th>
|
<th style=""padding:6px 10px;text-align:right"">{s.GridOut}</th>
|
||||||
<th style=""padding:6px 10px;text-align:right"">Batt In/Out</th>
|
<th style=""padding:6px 10px;text-align:right"">{s.BattInOut}</th>
|
||||||
</tr>
|
</tr>
|
||||||
{dailyRows}
|
{dailyRows}
|
||||||
</table>
|
</table>
|
||||||
|
|
@ -211,7 +372,7 @@ public static class ReportEmailService
|
||||||
<!-- Footer -->
|
<!-- Footer -->
|
||||||
<tr>
|
<tr>
|
||||||
<td style=""background:#f8f9fa;padding:16px 30px;text-align:center;font-size:12px;color:#999;border-top:1px solid #eee"">
|
<td style=""background:#f8f9fa;padding:16px 30px;text-align:center;font-size:12px;color:#999;border-top:1px solid #eee"">
|
||||||
Generated by <strong style=""color:#666"">Inesco Energy Monitor Platform</strong> · Powered by Mistral AI
|
{s.Footer}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -11,14 +11,17 @@ public static class WeeklyReportService
|
||||||
|
|
||||||
private static readonly ConcurrentDictionary<string, string> InsightCache = new();
|
private static readonly ConcurrentDictionary<string, string> InsightCache = new();
|
||||||
|
|
||||||
|
// Bump this version when the AI prompt changes to automatically invalidate old cache files
|
||||||
|
private const string CacheVersion = "v2";
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Generates a full weekly report for the given installation.
|
/// Generates a full weekly report for the given installation.
|
||||||
/// Caches the full report as JSON next to the xlsx. Cache is invalidated when xlsx is updated.
|
/// Caches the full report as JSON next to the xlsx. Cache is invalidated when xlsx is updated or CacheVersion changes.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static async Task<WeeklyReportResponse> GenerateReportAsync(long installationId, string installationName)
|
public static async Task<WeeklyReportResponse> GenerateReportAsync(long installationId, string installationName, string language = "en")
|
||||||
{
|
{
|
||||||
var xlsxPath = TmpReportDir + installationId + ".xlsx";
|
var xlsxPath = TmpReportDir + installationId + ".xlsx";
|
||||||
var cachePath = TmpReportDir + installationId + ".cache.json";
|
var cachePath = TmpReportDir + $"{installationId}_{language}_{CacheVersion}.cache.json";
|
||||||
|
|
||||||
// Use cached report if xlsx hasn't changed since cache was written
|
// Use cached report if xlsx hasn't changed since cache was written
|
||||||
if (File.Exists(cachePath) && File.Exists(xlsxPath))
|
if (File.Exists(cachePath) && File.Exists(xlsxPath))
|
||||||
|
|
@ -33,7 +36,7 @@ public static class WeeklyReportService
|
||||||
await File.ReadAllTextAsync(cachePath));
|
await File.ReadAllTextAsync(cachePath));
|
||||||
if (cached != null)
|
if (cached != null)
|
||||||
{
|
{
|
||||||
Console.WriteLine($"[WeeklyReportService] Returning cached report for installation {installationId}.");
|
Console.WriteLine($"[WeeklyReportService] Returning cached report for installation {installationId} ({language}).");
|
||||||
return cached;
|
return cached;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -45,7 +48,7 @@ public static class WeeklyReportService
|
||||||
}
|
}
|
||||||
|
|
||||||
var allDays = ExcelDataParser.Parse(xlsxPath);
|
var allDays = ExcelDataParser.Parse(xlsxPath);
|
||||||
var report = await GenerateReportFromDataAsync(allDays, installationName);
|
var report = await GenerateReportFromDataAsync(allDays, installationName, language);
|
||||||
|
|
||||||
// Write cache
|
// Write cache
|
||||||
try
|
try
|
||||||
|
|
@ -64,7 +67,7 @@ public static class WeeklyReportService
|
||||||
/// Core report generation from daily data. Data-source agnostic.
|
/// Core report generation from daily data. Data-source agnostic.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static async Task<WeeklyReportResponse> GenerateReportFromDataAsync(
|
public static async Task<WeeklyReportResponse> GenerateReportFromDataAsync(
|
||||||
List<DailyEnergyData> allDays, string installationName)
|
List<DailyEnergyData> allDays, string installationName, string language = "en")
|
||||||
{
|
{
|
||||||
// Sort by date
|
// Sort by date
|
||||||
allDays = allDays.OrderBy(d => d.Date).ToList();
|
allDays = allDays.OrderBy(d => d.Date).ToList();
|
||||||
|
|
@ -111,7 +114,7 @@ public static class WeeklyReportService
|
||||||
|
|
||||||
// AI insight
|
// AI insight
|
||||||
var aiInsight = await GetAiInsightAsync(currentWeekDays, currentSummary, previousSummary,
|
var aiInsight = await GetAiInsightAsync(currentWeekDays, currentSummary, previousSummary,
|
||||||
selfSufficiency, gridDependency, batteryEfficiency, installationName);
|
selfSufficiency, gridDependency, batteryEfficiency, installationName, language);
|
||||||
|
|
||||||
return new WeeklyReportResponse
|
return new WeeklyReportResponse
|
||||||
{
|
{
|
||||||
|
|
@ -152,6 +155,14 @@ public static class WeeklyReportService
|
||||||
|
|
||||||
private static readonly string MistralUrl = "https://api.mistral.ai/v1/chat/completions";
|
private static readonly string MistralUrl = "https://api.mistral.ai/v1/chat/completions";
|
||||||
|
|
||||||
|
private static string LanguageName(string code) => code switch
|
||||||
|
{
|
||||||
|
"de" => "German",
|
||||||
|
"fr" => "French",
|
||||||
|
"it" => "Italian",
|
||||||
|
_ => "English"
|
||||||
|
};
|
||||||
|
|
||||||
private static async Task<string> GetAiInsightAsync(
|
private static async Task<string> GetAiInsightAsync(
|
||||||
List<DailyEnergyData> currentWeek,
|
List<DailyEnergyData> currentWeek,
|
||||||
WeeklySummary current,
|
WeeklySummary current,
|
||||||
|
|
@ -159,7 +170,8 @@ public static class WeeklyReportService
|
||||||
double selfSufficiency,
|
double selfSufficiency,
|
||||||
double gridDependency,
|
double gridDependency,
|
||||||
double batteryEfficiency,
|
double batteryEfficiency,
|
||||||
string installationName)
|
string installationName,
|
||||||
|
string language = "en")
|
||||||
{
|
{
|
||||||
var apiKey = Environment.GetEnvironmentVariable("MISTRAL_API_KEY");
|
var apiKey = Environment.GetEnvironmentVariable("MISTRAL_API_KEY");
|
||||||
if (string.IsNullOrWhiteSpace(apiKey))
|
if (string.IsNullOrWhiteSpace(apiKey))
|
||||||
|
|
@ -168,8 +180,8 @@ public static class WeeklyReportService
|
||||||
return "AI insight unavailable (API key not configured).";
|
return "AI insight unavailable (API key not configured).";
|
||||||
}
|
}
|
||||||
|
|
||||||
// Cache key: installation + period
|
// Cache key: installation + period + language
|
||||||
var cacheKey = $"{installationName}_{currentWeek.Last().Date}";
|
var cacheKey = $"{installationName}_{currentWeek.Last().Date}_{language}";
|
||||||
if (InsightCache.TryGetValue(cacheKey, out var cached))
|
if (InsightCache.TryGetValue(cacheKey, out var cached))
|
||||||
return cached;
|
return cached;
|
||||||
|
|
||||||
|
|
@ -190,12 +202,15 @@ public static class WeeklyReportService
|
||||||
|
|
||||||
Write exactly 4 bullet points (each on its own line starting with ""- ""). No bold markers, no asterisks, no markdown — plain text only.
|
Write exactly 4 bullet points (each on its own line starting with ""- ""). No bold markers, no asterisks, no markdown — plain text only.
|
||||||
|
|
||||||
|
IMPORTANT FORMAT RULE: Each bullet MUST start with a short title followed by a colon, then the description. Example: ""- Title label: Description text here."" Translate the title label into {LanguageName(language)} but always keep the ""Title: description"" structure.
|
||||||
|
|
||||||
1. Solar savings: this week the system saved {solarSavings} kWh from the grid. Explain what this means in simple terms (e.g. equivalent to X days of average household use, or roughly X CHF saved at ~0.27 CHF/kWh).
|
1. Solar savings: this week the system saved {solarSavings} kWh from the grid. Explain what this means in simple terms (e.g. equivalent to X days of average household use, or roughly X CHF saved at ~0.27 CHF/kWh).
|
||||||
2. Best vs worst solar day: name the best and worst days with their PV kWh values. Mention likely weather reason.
|
2. Best vs worst solar day: name the best and worst days with their PV kWh values. Mention likely weather reason.
|
||||||
3. Battery performance: was the battery well-utilized this week? Mention charge/discharge totals and any standout days.
|
3. Battery performance: was the battery well-utilized this week? Mention charge/discharge totals and any standout days.
|
||||||
4. Tip of the week: one specific, practical recommendation based on THIS week's patterns to save more energy or money.
|
4. Tip of the week: one specific, practical recommendation based on THIS week's patterns to save more energy or money.
|
||||||
|
|
||||||
Rules: Use actual day names and numbers. Keep each bullet to 1-2 sentences. Write for a homeowner, not an engineer. Do NOT use asterisks or any formatting marks.
|
Rules: Use actual day names and numbers. Keep each bullet to 1-2 sentences. Write for a homeowner, not an engineer. Do NOT use asterisks or any formatting marks.
|
||||||
|
IMPORTANT: Write your entire response in {LanguageName(language)}.
|
||||||
|
|
||||||
Daily data (kWh):
|
Daily data (kWh):
|
||||||
{dayLines}
|
{dayLines}
|
||||||
|
|
|
||||||
|
|
@ -1,21 +1,22 @@
|
||||||
import { Navigate, Route, Routes, useNavigate } from 'react-router-dom';
|
import { Navigate, Route, Routes, useNavigate } from 'react-router-dom';
|
||||||
import { CssBaseline } from '@mui/material';
|
import { CssBaseline } from '@mui/material';
|
||||||
import ThemeProvider from './theme/ThemeProvider';
|
import ThemeProvider from './theme/ThemeProvider';
|
||||||
import React, { lazy, Suspense, useContext, useState } from 'react';
|
import React, { lazy, Suspense, useContext, useEffect, useState } from 'react';
|
||||||
import { UserContext } from './contexts/userContext';
|
import { UserContext } from './contexts/userContext';
|
||||||
import Login from './components/login';
|
import Login from './components/login';
|
||||||
import { IntlProvider } from 'react-intl';
|
import { IntlProvider } from 'react-intl';
|
||||||
import en from './lang/en.json';
|
import en from './lang/en.json';
|
||||||
import de from './lang/de.json';
|
import de from './lang/de.json';
|
||||||
import fr from './lang/fr.json';
|
import fr from './lang/fr.json';
|
||||||
|
import it from './lang/it.json';
|
||||||
import SuspenseLoader from './components/SuspenseLoader';
|
import SuspenseLoader from './components/SuspenseLoader';
|
||||||
|
import axiosConfig, { axiosConfigWithoutToken } from './Resources/axiosConfig';
|
||||||
import SidebarLayout from './layouts/SidebarLayout';
|
import SidebarLayout from './layouts/SidebarLayout';
|
||||||
import { TokenContext } from './contexts/tokenContext';
|
import { TokenContext } from './contexts/tokenContext';
|
||||||
import InstallationTabs from './content/dashboards/Installations/index';
|
import InstallationTabs from './content/dashboards/Installations/index';
|
||||||
import routes from 'src/Resources/routes.json';
|
import routes from 'src/Resources/routes.json';
|
||||||
import './App.css';
|
import './App.css';
|
||||||
import ForgotPassword from './components/ForgotPassword';
|
import ForgotPassword from './components/ForgotPassword';
|
||||||
import { axiosConfigWithoutToken } from './Resources/axiosConfig';
|
|
||||||
import InstallationsContextProvider from './contexts/InstallationsContextProvider';
|
import InstallationsContextProvider from './contexts/InstallationsContextProvider';
|
||||||
import AccessContextProvider from './contexts/AccessContextProvider';
|
import AccessContextProvider from './contexts/AccessContextProvider';
|
||||||
import SalidomoInstallationTabs from './content/dashboards/SalidomoInstallations';
|
import SalidomoInstallationTabs from './content/dashboards/SalidomoInstallations';
|
||||||
|
|
@ -37,15 +38,38 @@ function App() {
|
||||||
setAccessToSodistore
|
setAccessToSodistore
|
||||||
} = useContext(ProductIdContext);
|
} = useContext(ProductIdContext);
|
||||||
|
|
||||||
const [language, setLanguage] = useState('en');
|
const [language, setLanguage] = useState<string>(
|
||||||
|
() => localStorage.getItem('language') || currentUser?.language || 'en'
|
||||||
|
);
|
||||||
|
|
||||||
|
const onSelectLanguage = (lang: string) => {
|
||||||
|
setLanguage(lang);
|
||||||
|
localStorage.setItem('language', lang);
|
||||||
|
if (currentUser) {
|
||||||
|
const updatedUser = { ...currentUser, language: lang };
|
||||||
|
setUser(updatedUser);
|
||||||
|
axiosConfig.put('/UpdateUser', updatedUser).catch(() => {});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Sync localStorage language to DB when it differs (e.g. user changed language before new code was deployed)
|
||||||
|
useEffect(() => {
|
||||||
|
if (currentUser && token) {
|
||||||
|
const storedLang = localStorage.getItem('language');
|
||||||
|
if (storedLang && storedLang !== currentUser.language) {
|
||||||
|
const updatedUser = { ...currentUser, language: storedLang };
|
||||||
|
setUser(updatedUser);
|
||||||
|
axiosConfig.put('/UpdateUser', updatedUser).catch(() => {});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [token]);
|
||||||
|
|
||||||
const getTranslations = () => {
|
const getTranslations = () => {
|
||||||
switch (language) {
|
switch (language) {
|
||||||
case 'en':
|
case 'de': return de;
|
||||||
return en;
|
case 'fr': return fr;
|
||||||
case 'de':
|
case 'it': return it;
|
||||||
return de;
|
default: return en;
|
||||||
case 'fr':
|
|
||||||
return fr;
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -151,7 +175,7 @@ function App() {
|
||||||
element={
|
element={
|
||||||
<SidebarLayout
|
<SidebarLayout
|
||||||
language={language}
|
language={language}
|
||||||
onSelectLanguage={setLanguage}
|
onSelectLanguage={onSelectLanguage}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import React, { useContext, useEffect, useState } from 'react';
|
import React, { useContext, useEffect, useState } from 'react';
|
||||||
|
import { FormattedMessage, useIntl } from 'react-intl';
|
||||||
import { I_S3Credentials } from '../../../interfaces/S3Types';
|
import { I_S3Credentials } from '../../../interfaces/S3Types';
|
||||||
import {
|
import {
|
||||||
Box,
|
Box,
|
||||||
|
|
@ -36,6 +37,7 @@ function DetailedBatteryView(props: DetailedBatteryViewProps) {
|
||||||
if (props.batteryData === null) {
|
if (props.batteryData === null) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
const intl = useIntl();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const [openModalFirmwareUpdate, setOpenModalFirmwareUpdate] = useState(false);
|
const [openModalFirmwareUpdate, setOpenModalFirmwareUpdate] = useState(false);
|
||||||
const [openModalResultFirmwareUpdate, setOpenModalResultFirmwareUpdate] =
|
const [openModalResultFirmwareUpdate, setOpenModalResultFirmwareUpdate] =
|
||||||
|
|
@ -242,7 +244,7 @@ function DetailedBatteryView(props: DetailedBatteryViewProps) {
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error:', error.message);
|
console.error('Error:', error.message);
|
||||||
setErrorMessage('Download battery log failed, please try again.');
|
setErrorMessage(intl.formatMessage({ id: 'downloadBatteryLogFailed' }));
|
||||||
setOpenModalError(true);
|
setOpenModalError(true);
|
||||||
} finally {
|
} finally {
|
||||||
setOpenModalStartDownloadBatteryLog(false);
|
setOpenModalStartDownloadBatteryLog(false);
|
||||||
|
|
@ -282,7 +284,7 @@ function DetailedBatteryView(props: DetailedBatteryViewProps) {
|
||||||
gutterBottom
|
gutterBottom
|
||||||
sx={{ fontWeight: 'bold' }}
|
sx={{ fontWeight: 'bold' }}
|
||||||
>
|
>
|
||||||
The firmware is getting updated. Please wait...
|
<FormattedMessage id="firmwareUpdating" defaultMessage="The firmware is getting updated. Please wait..." />
|
||||||
</Typography>
|
</Typography>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
|
|
@ -302,7 +304,7 @@ function DetailedBatteryView(props: DetailedBatteryViewProps) {
|
||||||
}}
|
}}
|
||||||
onClick={firmwareModalResultHandleOk}
|
onClick={firmwareModalResultHandleOk}
|
||||||
>
|
>
|
||||||
Ok
|
<FormattedMessage id="ok" defaultMessage="Ok" />
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
@ -337,12 +339,11 @@ function DetailedBatteryView(props: DetailedBatteryViewProps) {
|
||||||
gutterBottom
|
gutterBottom
|
||||||
sx={{ fontWeight: 'bold' }}
|
sx={{ fontWeight: 'bold' }}
|
||||||
>
|
>
|
||||||
Do you really want to update the firmware?
|
<FormattedMessage id="confirmFirmwareUpdate" defaultMessage="Do you really want to update the firmware?" />
|
||||||
</Typography>
|
</Typography>
|
||||||
|
|
||||||
<Typography variant="body1" gutterBottom>
|
<Typography variant="body1" gutterBottom>
|
||||||
This action requires the battery service to be stopped for around
|
<FormattedMessage id="batteryServiceStopWarning" defaultMessage="This action requires the battery service to be stopped for around 10-15 minutes." />
|
||||||
10-15 minutes.
|
|
||||||
</Typography>
|
</Typography>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
|
|
@ -362,7 +363,7 @@ function DetailedBatteryView(props: DetailedBatteryViewProps) {
|
||||||
}}
|
}}
|
||||||
onClick={FirmwareModalHandleProceed}
|
onClick={FirmwareModalHandleProceed}
|
||||||
>
|
>
|
||||||
Proceed
|
<FormattedMessage id="proceed" defaultMessage="Proceed" />
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
sx={{
|
sx={{
|
||||||
|
|
@ -375,7 +376,7 @@ function DetailedBatteryView(props: DetailedBatteryViewProps) {
|
||||||
}}
|
}}
|
||||||
onClick={FirmwareModalHandleCancel}
|
onClick={FirmwareModalHandleCancel}
|
||||||
>
|
>
|
||||||
Cancel
|
<FormattedMessage id="cancel" defaultMessage="Cancel" />
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
@ -409,8 +410,7 @@ function DetailedBatteryView(props: DetailedBatteryViewProps) {
|
||||||
gutterBottom
|
gutterBottom
|
||||||
sx={{ fontWeight: 'bold' }}
|
sx={{ fontWeight: 'bold' }}
|
||||||
>
|
>
|
||||||
The battery log is getting downloaded. It will be saved in the
|
<FormattedMessage id="downloadingBatteryLog" defaultMessage="The battery log is getting downloaded. It will be saved in the Downloads folder. Please wait..." />
|
||||||
Downloads folder. Please wait...
|
|
||||||
</Typography>
|
</Typography>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
|
|
@ -430,7 +430,7 @@ function DetailedBatteryView(props: DetailedBatteryViewProps) {
|
||||||
}}
|
}}
|
||||||
onClick={StartDownloadBatteryLogModalHandleOk}
|
onClick={StartDownloadBatteryLogModalHandleOk}
|
||||||
>
|
>
|
||||||
Ok
|
<FormattedMessage id="ok" defaultMessage="Ok" />
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
@ -465,12 +465,11 @@ function DetailedBatteryView(props: DetailedBatteryViewProps) {
|
||||||
gutterBottom
|
gutterBottom
|
||||||
sx={{ fontWeight: 'bold' }}
|
sx={{ fontWeight: 'bold' }}
|
||||||
>
|
>
|
||||||
Do you really want to download battery log?
|
<FormattedMessage id="confirmBatteryLogDownload" defaultMessage="Do you really want to download battery log?" />
|
||||||
</Typography>
|
</Typography>
|
||||||
|
|
||||||
<Typography variant="body1" gutterBottom>
|
<Typography variant="body1" gutterBottom>
|
||||||
This action requires the battery service to be stopped for around
|
<FormattedMessage id="batteryServiceStopWarning" defaultMessage="This action requires the battery service to be stopped for around 10-15 minutes." />
|
||||||
10-15 minutes.
|
|
||||||
</Typography>
|
</Typography>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
|
|
@ -490,7 +489,7 @@ function DetailedBatteryView(props: DetailedBatteryViewProps) {
|
||||||
}}
|
}}
|
||||||
onClick={DownloadBatteryLogModalHandleProceed}
|
onClick={DownloadBatteryLogModalHandleProceed}
|
||||||
>
|
>
|
||||||
Proceed
|
<FormattedMessage id="proceed" defaultMessage="Proceed" />
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
sx={{
|
sx={{
|
||||||
|
|
@ -503,7 +502,7 @@ function DetailedBatteryView(props: DetailedBatteryViewProps) {
|
||||||
}}
|
}}
|
||||||
onClick={DownloadBatteryLogModalHandleCancel}
|
onClick={DownloadBatteryLogModalHandleCancel}
|
||||||
>
|
>
|
||||||
Cancel
|
<FormattedMessage id="cancel" defaultMessage="Cancel" />
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
@ -553,7 +552,7 @@ function DetailedBatteryView(props: DetailedBatteryViewProps) {
|
||||||
}}
|
}}
|
||||||
onClick={ErrorModalHandleOk}
|
onClick={ErrorModalHandleOk}
|
||||||
>
|
>
|
||||||
Ok
|
<FormattedMessage id="ok" defaultMessage="Ok" />
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import React, { useContext, useEffect, useState } from 'react';
|
import React, { useContext, useEffect, useState } from 'react';
|
||||||
|
import { FormattedMessage, useIntl } from 'react-intl';
|
||||||
import { I_S3Credentials } from '../../../interfaces/S3Types';
|
import { I_S3Credentials } from '../../../interfaces/S3Types';
|
||||||
import {
|
import {
|
||||||
Box,
|
Box,
|
||||||
|
|
@ -36,7 +37,7 @@ function DetailedBatteryViewSalidomo(props: DetailedBatteryViewProps) {
|
||||||
if (props.batteryData === null) {
|
if (props.batteryData === null) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
const intl = useIntl();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const [openModalFirmwareUpdate, setOpenModalFirmwareUpdate] = useState(false);
|
const [openModalFirmwareUpdate, setOpenModalFirmwareUpdate] = useState(false);
|
||||||
const [openModalResultFirmwareUpdate, setOpenModalResultFirmwareUpdate] =
|
const [openModalResultFirmwareUpdate, setOpenModalResultFirmwareUpdate] =
|
||||||
|
|
@ -243,7 +244,7 @@ function DetailedBatteryViewSalidomo(props: DetailedBatteryViewProps) {
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error:', error.message);
|
console.error('Error:', error.message);
|
||||||
setErrorMessage('Download battery log failed, please try again.');
|
setErrorMessage(intl.formatMessage({ id: 'downloadBatteryLogFailed' }));
|
||||||
setOpenModalError(true);
|
setOpenModalError(true);
|
||||||
} finally {
|
} finally {
|
||||||
setOpenModalStartDownloadBatteryLog(false);
|
setOpenModalStartDownloadBatteryLog(false);
|
||||||
|
|
@ -283,7 +284,7 @@ function DetailedBatteryViewSalidomo(props: DetailedBatteryViewProps) {
|
||||||
gutterBottom
|
gutterBottom
|
||||||
sx={{ fontWeight: 'bold' }}
|
sx={{ fontWeight: 'bold' }}
|
||||||
>
|
>
|
||||||
The firmware is getting updated. Please wait...
|
<FormattedMessage id="firmwareUpdating" defaultMessage="The firmware is getting updated. Please wait..." />
|
||||||
</Typography>
|
</Typography>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
|
|
@ -303,7 +304,7 @@ function DetailedBatteryViewSalidomo(props: DetailedBatteryViewProps) {
|
||||||
}}
|
}}
|
||||||
onClick={firmwareModalResultHandleOk}
|
onClick={firmwareModalResultHandleOk}
|
||||||
>
|
>
|
||||||
Ok
|
<FormattedMessage id="ok" defaultMessage="Ok" />
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
@ -338,12 +339,11 @@ function DetailedBatteryViewSalidomo(props: DetailedBatteryViewProps) {
|
||||||
gutterBottom
|
gutterBottom
|
||||||
sx={{ fontWeight: 'bold' }}
|
sx={{ fontWeight: 'bold' }}
|
||||||
>
|
>
|
||||||
Do you really want to update the firmware?
|
<FormattedMessage id="confirmFirmwareUpdate" defaultMessage="Do you really want to update the firmware?" />
|
||||||
</Typography>
|
</Typography>
|
||||||
|
|
||||||
<Typography variant="body1" gutterBottom>
|
<Typography variant="body1" gutterBottom>
|
||||||
This action requires the battery service to be stopped for around
|
<FormattedMessage id="batteryServiceStopWarning" defaultMessage="This action requires the battery service to be stopped for around 10-15 minutes." />
|
||||||
10-15 minutes.
|
|
||||||
</Typography>
|
</Typography>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
|
|
@ -363,7 +363,7 @@ function DetailedBatteryViewSalidomo(props: DetailedBatteryViewProps) {
|
||||||
}}
|
}}
|
||||||
onClick={FirmwareModalHandleProceed}
|
onClick={FirmwareModalHandleProceed}
|
||||||
>
|
>
|
||||||
Proceed
|
<FormattedMessage id="proceed" defaultMessage="Proceed" />
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
sx={{
|
sx={{
|
||||||
|
|
@ -376,7 +376,7 @@ function DetailedBatteryViewSalidomo(props: DetailedBatteryViewProps) {
|
||||||
}}
|
}}
|
||||||
onClick={FirmwareModalHandleCancel}
|
onClick={FirmwareModalHandleCancel}
|
||||||
>
|
>
|
||||||
Cancel
|
<FormattedMessage id="cancel" defaultMessage="Cancel" />
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
@ -410,8 +410,7 @@ function DetailedBatteryViewSalidomo(props: DetailedBatteryViewProps) {
|
||||||
gutterBottom
|
gutterBottom
|
||||||
sx={{ fontWeight: 'bold' }}
|
sx={{ fontWeight: 'bold' }}
|
||||||
>
|
>
|
||||||
The battery log is getting downloaded. It will be saved in the
|
<FormattedMessage id="downloadingBatteryLog" defaultMessage="The battery log is getting downloaded. It will be saved in the Downloads folder. Please wait..." />
|
||||||
Downloads folder. Please wait...
|
|
||||||
</Typography>
|
</Typography>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
|
|
@ -431,7 +430,7 @@ function DetailedBatteryViewSalidomo(props: DetailedBatteryViewProps) {
|
||||||
}}
|
}}
|
||||||
onClick={StartDownloadBatteryLogModalHandleOk}
|
onClick={StartDownloadBatteryLogModalHandleOk}
|
||||||
>
|
>
|
||||||
Ok
|
<FormattedMessage id="ok" defaultMessage="Ok" />
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
@ -466,12 +465,11 @@ function DetailedBatteryViewSalidomo(props: DetailedBatteryViewProps) {
|
||||||
gutterBottom
|
gutterBottom
|
||||||
sx={{ fontWeight: 'bold' }}
|
sx={{ fontWeight: 'bold' }}
|
||||||
>
|
>
|
||||||
Do you really want to download battery log?
|
<FormattedMessage id="confirmBatteryLogDownload" defaultMessage="Do you really want to download battery log?" />
|
||||||
</Typography>
|
</Typography>
|
||||||
|
|
||||||
<Typography variant="body1" gutterBottom>
|
<Typography variant="body1" gutterBottom>
|
||||||
This action requires the battery service to be stopped for around
|
<FormattedMessage id="batteryServiceStopWarning" defaultMessage="This action requires the battery service to be stopped for around 10-15 minutes." />
|
||||||
10-15 minutes.
|
|
||||||
</Typography>
|
</Typography>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
|
|
@ -491,7 +489,7 @@ function DetailedBatteryViewSalidomo(props: DetailedBatteryViewProps) {
|
||||||
}}
|
}}
|
||||||
onClick={DownloadBatteryLogModalHandleProceed}
|
onClick={DownloadBatteryLogModalHandleProceed}
|
||||||
>
|
>
|
||||||
Proceed
|
<FormattedMessage id="proceed" defaultMessage="Proceed" />
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
sx={{
|
sx={{
|
||||||
|
|
@ -504,7 +502,7 @@ function DetailedBatteryViewSalidomo(props: DetailedBatteryViewProps) {
|
||||||
}}
|
}}
|
||||||
onClick={DownloadBatteryLogModalHandleCancel}
|
onClick={DownloadBatteryLogModalHandleCancel}
|
||||||
>
|
>
|
||||||
Cancel
|
<FormattedMessage id="cancel" defaultMessage="Cancel" />
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
@ -554,7 +552,7 @@ function DetailedBatteryViewSalidomo(props: DetailedBatteryViewProps) {
|
||||||
}}
|
}}
|
||||||
onClick={ErrorModalHandleOk}
|
onClick={ErrorModalHandleOk}
|
||||||
>
|
>
|
||||||
Ok
|
<FormattedMessage id="ok" defaultMessage="Ok" />
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
|
||||||
|
|
@ -494,7 +494,7 @@ function Log(props: LogProps) {
|
||||||
{diag.description}
|
{diag.description}
|
||||||
</Typography>
|
</Typography>
|
||||||
<Typography variant="caption" color="text.secondary">
|
<Typography variant="caption" color="text.secondary">
|
||||||
Last seen: {diag.lastSeen}
|
<FormattedMessage id="lastSeen" defaultMessage="Last seen" />: {diag.lastSeen}
|
||||||
</Typography>
|
</Typography>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -26,7 +26,7 @@ import PersonIcon from '@mui/icons-material/Person';
|
||||||
import Button from '@mui/material/Button';
|
import Button from '@mui/material/Button';
|
||||||
import { Close as CloseIcon } from '@mui/icons-material';
|
import { Close as CloseIcon } from '@mui/icons-material';
|
||||||
import { AccessContext } from 'src/contexts/AccessContextProvider';
|
import { AccessContext } from 'src/contexts/AccessContextProvider';
|
||||||
import { FormattedMessage } from 'react-intl';
|
import { FormattedMessage, useIntl } from 'react-intl';
|
||||||
import { UserType } from '../../../interfaces/UserTypes';
|
import { UserType } from '../../../interfaces/UserTypes';
|
||||||
|
|
||||||
interface AccessProps {
|
interface AccessProps {
|
||||||
|
|
@ -35,6 +35,7 @@ interface AccessProps {
|
||||||
}
|
}
|
||||||
|
|
||||||
function Access(props: AccessProps) {
|
function Access(props: AccessProps) {
|
||||||
|
const intl = useIntl();
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
const tokencontext = useContext(TokenContext);
|
const tokencontext = useContext(TokenContext);
|
||||||
const { removeToken } = tokencontext;
|
const { removeToken } = tokencontext;
|
||||||
|
|
@ -159,31 +160,11 @@ function Access(props: AccessProps) {
|
||||||
|
|
||||||
if (NotGrantedAccessUsers.length > 0) {
|
if (NotGrantedAccessUsers.length > 0) {
|
||||||
setError(true);
|
setError(true);
|
||||||
|
setErrorMessage(intl.formatMessage({ id: 'unableToGrantAccess' }) + ' ' + NotGrantedAccessUsers.join(', '));
|
||||||
const message =
|
|
||||||
(
|
|
||||||
<FormattedMessage
|
|
||||||
id="unableToGrantAccess"
|
|
||||||
defaultMessage="Unable to grant access to: "
|
|
||||||
/>
|
|
||||||
).props.defaultMessage +
|
|
||||||
' ' +
|
|
||||||
NotGrantedAccessUsers.join(', ');
|
|
||||||
|
|
||||||
setErrorMessage(message);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (grantedAccessUsers.length > 0) {
|
if (grantedAccessUsers.length > 0) {
|
||||||
const message =
|
setUpdatedMessage(intl.formatMessage({ id: 'grantedAccessToUsers' }) + ' ' + grantedAccessUsers.join(', '));
|
||||||
(
|
|
||||||
<FormattedMessage
|
|
||||||
id="grantedAccessToUsers"
|
|
||||||
defaultMessage="Granted access to users: "
|
|
||||||
/>
|
|
||||||
).props.defaultMessage +
|
|
||||||
' ' +
|
|
||||||
grantedAccessUsers.join(', ');
|
|
||||||
setUpdatedMessage(message);
|
|
||||||
|
|
||||||
setUpdated(true);
|
setUpdated(true);
|
||||||
|
|
||||||
|
|
@ -306,7 +287,7 @@ function Access(props: AccessProps) {
|
||||||
}}
|
}}
|
||||||
onClick={handleCloseFolder}
|
onClick={handleCloseFolder}
|
||||||
>
|
>
|
||||||
Ok
|
<FormattedMessage id="ok" defaultMessage="Ok" />
|
||||||
</Button>
|
</Button>
|
||||||
</Select>
|
</Select>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
|
|
|
||||||
|
|
@ -29,7 +29,7 @@ import PersonIcon from '@mui/icons-material/Person';
|
||||||
import Button from '@mui/material/Button';
|
import Button from '@mui/material/Button';
|
||||||
import { Close as CloseIcon } from '@mui/icons-material';
|
import { Close as CloseIcon } from '@mui/icons-material';
|
||||||
import { AccessContext } from 'src/contexts/AccessContextProvider';
|
import { AccessContext } from 'src/contexts/AccessContextProvider';
|
||||||
import { FormattedMessage } from 'react-intl';
|
import { FormattedMessage, useIntl } from 'react-intl';
|
||||||
import { InnovEnergyUser, UserType } from '../../../interfaces/UserTypes';
|
import { InnovEnergyUser, UserType } from '../../../interfaces/UserTypes';
|
||||||
import PersonRemoveIcon from '@mui/icons-material/PersonRemove';
|
import PersonRemoveIcon from '@mui/icons-material/PersonRemove';
|
||||||
import {
|
import {
|
||||||
|
|
@ -47,6 +47,7 @@ function UserAccess(props: UserAccessProps) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const intl = useIntl();
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
const tokencontext = useContext(TokenContext);
|
const tokencontext = useContext(TokenContext);
|
||||||
const { removeToken } = tokencontext;
|
const { removeToken } = tokencontext;
|
||||||
|
|
@ -165,20 +166,12 @@ function UserAccess(props: UserAccessProps) {
|
||||||
)
|
)
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
if (response) {
|
if (response) {
|
||||||
setUpdatedMessage(
|
setUpdatedMessage(intl.formatMessage({ id: 'grantedAccessToUser' }, { name: props.current_user.name }));
|
||||||
'Granted access to user ' + props.current_user.name
|
|
||||||
);
|
|
||||||
setUpdated(true);
|
setUpdated(true);
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
if (err.response && err.response.status === 401) {
|
setErrorMessage(intl.formatMessage({ id: 'errorOccured' }));
|
||||||
setErrorMessage(
|
|
||||||
`User ${props.current_user.name} already has access to folder "${folder.name}" or you don't have permission to grant this access`
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
setErrorMessage('An error has occured');
|
|
||||||
}
|
|
||||||
setError(true);
|
setError(true);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
@ -194,20 +187,12 @@ function UserAccess(props: UserAccessProps) {
|
||||||
)
|
)
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
if (response) {
|
if (response) {
|
||||||
setUpdatedMessage(
|
setUpdatedMessage(intl.formatMessage({ id: 'grantedAccessToUser' }, { name: props.current_user.name }));
|
||||||
'Granted access to user ' + props.current_user.name
|
|
||||||
);
|
|
||||||
setUpdated(true);
|
setUpdated(true);
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
if (err.response && err.response.status === 401) {
|
setErrorMessage(intl.formatMessage({ id: 'errorOccured' }));
|
||||||
setErrorMessage(
|
|
||||||
`User ${props.current_user.name} already has access to installation "${installation.name}" or you don't have permission to grant this access`
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
setErrorMessage('An error has occured');
|
|
||||||
}
|
|
||||||
setError(true);
|
setError(true);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -621,7 +621,7 @@ function Overview(props: OverviewProps) {
|
||||||
</Container>
|
</Container>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{!loading && (
|
{!loading && dailyDataArray.length > 0 && (
|
||||||
<Grid item xs={12} md={12}>
|
<Grid item xs={12} md={12}>
|
||||||
{dailyData && (
|
{dailyData && (
|
||||||
<Grid
|
<Grid
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
|
import { useIntl, FormattedMessage } from 'react-intl';
|
||||||
import {
|
import {
|
||||||
Box,
|
Box,
|
||||||
Button,
|
Button,
|
||||||
|
|
@ -77,6 +78,7 @@ function FormattedBullet({ text }: { text: string }) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function WeeklyReport({ installationId }: WeeklyReportProps) {
|
function WeeklyReport({ installationId }: WeeklyReportProps) {
|
||||||
|
const intl = useIntl();
|
||||||
const [report, setReport] = useState<WeeklyReportResponse | null>(null);
|
const [report, setReport] = useState<WeeklyReportResponse | null>(null);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
|
@ -89,14 +91,14 @@ function WeeklyReport({ installationId }: WeeklyReportProps) {
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchReport();
|
fetchReport();
|
||||||
}, [installationId]);
|
}, [installationId, intl.locale]);
|
||||||
|
|
||||||
const fetchReport = async () => {
|
const fetchReport = async () => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
setError(null);
|
setError(null);
|
||||||
try {
|
try {
|
||||||
const res = await axiosConfig.get('/GetWeeklyReport', {
|
const res = await axiosConfig.get('/GetWeeklyReport', {
|
||||||
params: { installationId }
|
params: { installationId, language: intl.locale }
|
||||||
});
|
});
|
||||||
setReport(res.data);
|
setReport(res.data);
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
|
|
@ -117,9 +119,9 @@ function WeeklyReport({ installationId }: WeeklyReportProps) {
|
||||||
await axiosConfig.post('/SendWeeklyReportEmail', null, {
|
await axiosConfig.post('/SendWeeklyReportEmail', null, {
|
||||||
params: { installationId, emailAddress: email.trim() }
|
params: { installationId, emailAddress: email.trim() }
|
||||||
});
|
});
|
||||||
setSendStatus({ message: `Report sent to ${email}`, severity: 'success' });
|
setSendStatus({ message: intl.formatMessage({ id: 'reportSentTo' }, { email }), severity: 'success' });
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
setSendStatus({ message: 'Failed to send. Please check the email address and try again.', severity: 'error' });
|
setSendStatus({ message: intl.formatMessage({ id: 'reportSendError' }), severity: 'error' });
|
||||||
} finally {
|
} finally {
|
||||||
setSending(false);
|
setSending(false);
|
||||||
}
|
}
|
||||||
|
|
@ -138,7 +140,7 @@ function WeeklyReport({ installationId }: WeeklyReportProps) {
|
||||||
>
|
>
|
||||||
<CircularProgress size={50} style={{ color: '#ffc04d' }} />
|
<CircularProgress size={50} style={{ color: '#ffc04d' }} />
|
||||||
<Typography variant="body2" mt={2}>
|
<Typography variant="body2" mt={2}>
|
||||||
Generating weekly report...
|
<FormattedMessage id="generatingReport" defaultMessage="Generating weekly report..." />
|
||||||
</Typography>
|
</Typography>
|
||||||
</Container>
|
</Container>
|
||||||
);
|
);
|
||||||
|
|
@ -147,7 +149,7 @@ function WeeklyReport({ installationId }: WeeklyReportProps) {
|
||||||
if (error) {
|
if (error) {
|
||||||
return (
|
return (
|
||||||
<Container sx={{ py: 4 }}>
|
<Container sx={{ py: 4 }}>
|
||||||
<Alert severity="warning">{error}</Alert>
|
<Alert severity="warning"><FormattedMessage id="noReportData" defaultMessage="No report data found." /></Alert>
|
||||||
</Container>
|
</Container>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -201,7 +203,7 @@ function WeeklyReport({ installationId }: WeeklyReportProps) {
|
||||||
disabled={sending || !email.trim()}
|
disabled={sending || !email.trim()}
|
||||||
sx={{ bgcolor: '#2c3e50', '&:hover': { bgcolor: '#34495e' } }}
|
sx={{ bgcolor: '#2c3e50', '&:hover': { bgcolor: '#34495e' } }}
|
||||||
>
|
>
|
||||||
Send Report
|
<FormattedMessage id="sendReport" defaultMessage="Send Report" />
|
||||||
</Button>
|
</Button>
|
||||||
</Box>
|
</Box>
|
||||||
{sendStatus && (
|
{sendStatus && (
|
||||||
|
|
@ -222,7 +224,7 @@ function WeeklyReport({ installationId }: WeeklyReportProps) {
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Typography variant="h5" fontWeight="bold">
|
<Typography variant="h5" fontWeight="bold">
|
||||||
Weekly Performance Report
|
<FormattedMessage id="reportTitle" defaultMessage="Weekly Performance Report" />
|
||||||
</Typography>
|
</Typography>
|
||||||
<Typography variant="body1" sx={{ opacity: 0.9, mt: 0.5 }}>
|
<Typography variant="body1" sx={{ opacity: 0.9, mt: 0.5 }}>
|
||||||
{report.installationName}
|
{report.installationName}
|
||||||
|
|
@ -235,7 +237,7 @@ function WeeklyReport({ installationId }: WeeklyReportProps) {
|
||||||
{/* Weekly Insights (was AI Insights) */}
|
{/* Weekly Insights (was AI Insights) */}
|
||||||
<Paper sx={{ p: 3, mb: 3 }}>
|
<Paper sx={{ p: 3, mb: 3 }}>
|
||||||
<Typography variant="h6" fontWeight="bold" sx={{ mb: 2, color: '#2c3e50' }}>
|
<Typography variant="h6" fontWeight="bold" sx={{ mb: 2, color: '#2c3e50' }}>
|
||||||
Weekly Insights
|
<FormattedMessage id="weeklyInsights" defaultMessage="Weekly Insights" />
|
||||||
</Typography>
|
</Typography>
|
||||||
<Box
|
<Box
|
||||||
sx={{
|
sx={{
|
||||||
|
|
@ -262,38 +264,38 @@ function WeeklyReport({ installationId }: WeeklyReportProps) {
|
||||||
{/* Your Savings This Week */}
|
{/* Your Savings This Week */}
|
||||||
<Paper sx={{ p: 3, mb: 3 }}>
|
<Paper sx={{ p: 3, mb: 3 }}>
|
||||||
<Typography variant="h6" fontWeight="bold" sx={{ mb: 2, color: '#2c3e50' }}>
|
<Typography variant="h6" fontWeight="bold" sx={{ mb: 2, color: '#2c3e50' }}>
|
||||||
Your Savings This Week
|
<FormattedMessage id="weeklySavings" defaultMessage="Your Savings This Week" />
|
||||||
</Typography>
|
</Typography>
|
||||||
<Grid container spacing={2}>
|
<Grid container spacing={2}>
|
||||||
<Grid item xs={6} sm={3}>
|
<Grid item xs={6} sm={3}>
|
||||||
<SavingsCard
|
<SavingsCard
|
||||||
label="Solar Energy Used"
|
label={intl.formatMessage({ id: 'solarEnergyUsed' })}
|
||||||
value={`${solarSavingsKwh} kWh`}
|
value={`${solarSavingsKwh} kWh`}
|
||||||
subtitle="of your solar stayed at home"
|
subtitle={intl.formatMessage({ id: 'solarStayedHome' })}
|
||||||
color="#27ae60"
|
color="#27ae60"
|
||||||
/>
|
/>
|
||||||
</Grid>
|
</Grid>
|
||||||
<Grid item xs={6} sm={3}>
|
<Grid item xs={6} sm={3}>
|
||||||
<SavingsCard
|
<SavingsCard
|
||||||
label="Est. Money Saved"
|
label={intl.formatMessage({ id: 'estMoneySaved' })}
|
||||||
value={`~${estimatedSavingsCHF} CHF`}
|
value={`~${estimatedSavingsCHF} CHF`}
|
||||||
subtitle="at 0.27 CHF/kWh avg."
|
subtitle={intl.formatMessage({ id: 'atCHFRate' })}
|
||||||
color="#2980b9"
|
color="#2980b9"
|
||||||
/>
|
/>
|
||||||
</Grid>
|
</Grid>
|
||||||
<Grid item xs={6} sm={3}>
|
<Grid item xs={6} sm={3}>
|
||||||
<SavingsCard
|
<SavingsCard
|
||||||
label="Solar Coverage"
|
label={intl.formatMessage({ id: 'solarCoverage' })}
|
||||||
value={`${report.selfSufficiencyPercent.toFixed(0)}%`}
|
value={`${report.selfSufficiencyPercent.toFixed(0)}%`}
|
||||||
subtitle="of consumption from solar"
|
subtitle={intl.formatMessage({ id: 'fromSolarSub' })}
|
||||||
color="#8e44ad"
|
color="#8e44ad"
|
||||||
/>
|
/>
|
||||||
</Grid>
|
</Grid>
|
||||||
<Grid item xs={6} sm={3}>
|
<Grid item xs={6} sm={3}>
|
||||||
<SavingsCard
|
<SavingsCard
|
||||||
label="Battery Efficiency"
|
label={intl.formatMessage({ id: 'batteryEfficiency' })}
|
||||||
value={`${report.batteryEfficiencyPercent.toFixed(0)}%`}
|
value={`${report.batteryEfficiencyPercent.toFixed(0)}%`}
|
||||||
subtitle="energy out vs energy in"
|
subtitle={intl.formatMessage({ id: 'batteryEffSub' })}
|
||||||
color="#e67e22"
|
color="#e67e22"
|
||||||
/>
|
/>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|
@ -303,44 +305,44 @@ function WeeklyReport({ installationId }: WeeklyReportProps) {
|
||||||
{/* Weekly Summary Table */}
|
{/* Weekly Summary Table */}
|
||||||
<Paper sx={{ p: 3, mb: 3 }}>
|
<Paper sx={{ p: 3, mb: 3 }}>
|
||||||
<Typography variant="h6" fontWeight="bold" sx={{ mb: 2, color: '#2c3e50' }}>
|
<Typography variant="h6" fontWeight="bold" sx={{ mb: 2, color: '#2c3e50' }}>
|
||||||
Weekly Summary
|
<FormattedMessage id="weeklySummary" defaultMessage="Weekly Summary" />
|
||||||
</Typography>
|
</Typography>
|
||||||
<Box component="table" sx={{ width: '100%', borderCollapse: 'collapse', '& td, & th': { p: 1.2, borderBottom: '1px solid #eee' } }}>
|
<Box component="table" sx={{ width: '100%', borderCollapse: 'collapse', '& td, & th': { p: 1.2, borderBottom: '1px solid #eee' } }}>
|
||||||
<thead>
|
<thead>
|
||||||
<tr style={{ background: '#f8f9fa' }}>
|
<tr style={{ background: '#f8f9fa' }}>
|
||||||
<th style={{ textAlign: 'left' }}>Metric</th>
|
<th style={{ textAlign: 'left' }}><FormattedMessage id="metric" defaultMessage="Metric" /></th>
|
||||||
<th style={{ textAlign: 'right' }}>This Week</th>
|
<th style={{ textAlign: 'right' }}><FormattedMessage id="thisWeek" defaultMessage="This Week" /></th>
|
||||||
{prev && <th style={{ textAlign: 'right' }}>Last Week</th>}
|
{prev && <th style={{ textAlign: 'right' }}><FormattedMessage id="lastWeek" defaultMessage="Last Week" /></th>}
|
||||||
{prev && <th style={{ textAlign: 'right' }}>Change</th>}
|
{prev && <th style={{ textAlign: 'right' }}><FormattedMessage id="change" defaultMessage="Change" /></th>}
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr>
|
<tr>
|
||||||
<td>PV Production</td>
|
<td><FormattedMessage id="pvProduction" defaultMessage="PV Production" /></td>
|
||||||
<td style={{ textAlign: 'right', fontWeight: 'bold' }}>{cur.totalPvProduction.toFixed(1)} kWh</td>
|
<td style={{ textAlign: 'right', fontWeight: 'bold' }}>{cur.totalPvProduction.toFixed(1)} kWh</td>
|
||||||
{prev && <td style={{ textAlign: 'right', color: '#888' }}>{prev.totalPvProduction.toFixed(1)} kWh</td>}
|
{prev && <td style={{ textAlign: 'right', color: '#888' }}>{prev.totalPvProduction.toFixed(1)} kWh</td>}
|
||||||
{prev && <td style={{ textAlign: 'right', color: changeColor(report.pvChangePercent) }}>{formatChange(report.pvChangePercent)}</td>}
|
{prev && <td style={{ textAlign: 'right', color: changeColor(report.pvChangePercent) }}>{formatChange(report.pvChangePercent)}</td>}
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>Consumption</td>
|
<td><FormattedMessage id="consumption" defaultMessage="Consumption" /></td>
|
||||||
<td style={{ textAlign: 'right', fontWeight: 'bold' }}>{cur.totalConsumption.toFixed(1)} kWh</td>
|
<td style={{ textAlign: 'right', fontWeight: 'bold' }}>{cur.totalConsumption.toFixed(1)} kWh</td>
|
||||||
{prev && <td style={{ textAlign: 'right', color: '#888' }}>{prev.totalConsumption.toFixed(1)} kWh</td>}
|
{prev && <td style={{ textAlign: 'right', color: '#888' }}>{prev.totalConsumption.toFixed(1)} kWh</td>}
|
||||||
{prev && <td style={{ textAlign: 'right', color: changeColor(report.consumptionChangePercent, true) }}>{formatChange(report.consumptionChangePercent)}</td>}
|
{prev && <td style={{ textAlign: 'right', color: changeColor(report.consumptionChangePercent, true) }}>{formatChange(report.consumptionChangePercent)}</td>}
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>Grid Import</td>
|
<td><FormattedMessage id="gridImport" defaultMessage="Grid Import" /></td>
|
||||||
<td style={{ textAlign: 'right', fontWeight: 'bold' }}>{cur.totalGridImport.toFixed(1)} kWh</td>
|
<td style={{ textAlign: 'right', fontWeight: 'bold' }}>{cur.totalGridImport.toFixed(1)} kWh</td>
|
||||||
{prev && <td style={{ textAlign: 'right', color: '#888' }}>{prev.totalGridImport.toFixed(1)} kWh</td>}
|
{prev && <td style={{ textAlign: 'right', color: '#888' }}>{prev.totalGridImport.toFixed(1)} kWh</td>}
|
||||||
{prev && <td style={{ textAlign: 'right', color: changeColor(report.gridImportChangePercent, true) }}>{formatChange(report.gridImportChangePercent)}</td>}
|
{prev && <td style={{ textAlign: 'right', color: changeColor(report.gridImportChangePercent, true) }}>{formatChange(report.gridImportChangePercent)}</td>}
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>Grid Export</td>
|
<td><FormattedMessage id="gridExport" defaultMessage="Grid Export" /></td>
|
||||||
<td style={{ textAlign: 'right', fontWeight: 'bold' }}>{cur.totalGridExport.toFixed(1)} kWh</td>
|
<td style={{ textAlign: 'right', fontWeight: 'bold' }}>{cur.totalGridExport.toFixed(1)} kWh</td>
|
||||||
{prev && <td style={{ textAlign: 'right', color: '#888' }}>{prev.totalGridExport.toFixed(1)} kWh</td>}
|
{prev && <td style={{ textAlign: 'right', color: '#888' }}>{prev.totalGridExport.toFixed(1)} kWh</td>}
|
||||||
{prev && <td style={{ textAlign: 'right' }}>—</td>}
|
{prev && <td style={{ textAlign: 'right' }}>—</td>}
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>Battery In / Out</td>
|
<td><FormattedMessage id="batteryInOut" defaultMessage="Battery In / Out" /></td>
|
||||||
<td style={{ textAlign: 'right', fontWeight: 'bold' }}>{cur.totalBatteryCharged.toFixed(1)} / {cur.totalBatteryDischarged.toFixed(1)} kWh</td>
|
<td style={{ textAlign: 'right', fontWeight: 'bold' }}>{cur.totalBatteryCharged.toFixed(1)} / {cur.totalBatteryDischarged.toFixed(1)} kWh</td>
|
||||||
{prev && <td style={{ textAlign: 'right', color: '#888' }}>{prev.totalBatteryCharged.toFixed(1)} / {prev.totalBatteryDischarged.toFixed(1)} kWh</td>}
|
{prev && <td style={{ textAlign: 'right', color: '#888' }}>{prev.totalBatteryCharged.toFixed(1)} / {prev.totalBatteryDischarged.toFixed(1)} kWh</td>}
|
||||||
{prev && <td style={{ textAlign: 'right' }}>—</td>}
|
{prev && <td style={{ textAlign: 'right' }}>—</td>}
|
||||||
|
|
@ -353,18 +355,18 @@ function WeeklyReport({ installationId }: WeeklyReportProps) {
|
||||||
{report.dailyData.length > 0 && (
|
{report.dailyData.length > 0 && (
|
||||||
<Paper sx={{ p: 3, mb: 3 }}>
|
<Paper sx={{ p: 3, mb: 3 }}>
|
||||||
<Typography variant="h6" fontWeight="bold" sx={{ mb: 1, color: '#2c3e50' }}>
|
<Typography variant="h6" fontWeight="bold" sx={{ mb: 1, color: '#2c3e50' }}>
|
||||||
Daily Breakdown
|
<FormattedMessage id="dailyBreakdown" defaultMessage="Daily Breakdown" />
|
||||||
</Typography>
|
</Typography>
|
||||||
{/* Legend */}
|
{/* Legend */}
|
||||||
<Box sx={{ display: 'flex', gap: 3, mb: 2, fontSize: '12px', color: '#666' }}>
|
<Box sx={{ display: 'flex', gap: 3, mb: 2, fontSize: '12px', color: '#666' }}>
|
||||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5 }}>
|
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5 }}>
|
||||||
<Box sx={{ width: 12, height: 12, bgcolor: '#f39c12', borderRadius: '2px' }} /> PV Production
|
<Box sx={{ width: 12, height: 12, bgcolor: '#f39c12', borderRadius: '2px' }} /> <FormattedMessage id="pvProduction" defaultMessage="PV Production" />
|
||||||
</Box>
|
</Box>
|
||||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5 }}>
|
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5 }}>
|
||||||
<Box sx={{ width: 12, height: 12, bgcolor: '#3498db', borderRadius: '2px' }} /> Consumption
|
<Box sx={{ width: 12, height: 12, bgcolor: '#3498db', borderRadius: '2px' }} /> <FormattedMessage id="consumption" defaultMessage="Consumption" />
|
||||||
</Box>
|
</Box>
|
||||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5 }}>
|
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5 }}>
|
||||||
<Box sx={{ width: 12, height: 12, bgcolor: '#e74c3c', borderRadius: '2px' }} /> Grid Import
|
<Box sx={{ width: 12, height: 12, bgcolor: '#e74c3c', borderRadius: '2px' }} /> <FormattedMessage id="gridImport" defaultMessage="Grid Import" />
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
{/* Bars */}
|
{/* Bars */}
|
||||||
|
|
@ -377,7 +379,7 @@ function WeeklyReport({ installationId }: WeeklyReportProps) {
|
||||||
<Box sx={{ display: 'flex', justifyContent: 'space-between', mb: 0.3 }}>
|
<Box sx={{ display: 'flex', justifyContent: 'space-between', mb: 0.3 }}>
|
||||||
<Typography variant="caption" sx={{ fontWeight: isCurrentWeek ? 'bold' : 'normal', color: '#444', fontSize: '12px' }}>
|
<Typography variant="caption" sx={{ fontWeight: isCurrentWeek ? 'bold' : 'normal', color: '#444', fontSize: '12px' }}>
|
||||||
{dayLabel}
|
{dayLabel}
|
||||||
{!isCurrentWeek && <span style={{ color: '#999', marginLeft: 4 }}>(prev week)</span>}
|
{!isCurrentWeek && <span style={{ color: '#999', marginLeft: 4 }}><FormattedMessage id="prevWeek" defaultMessage="(prev week)" /></span>}
|
||||||
</Typography>
|
</Typography>
|
||||||
<Typography variant="caption" sx={{ color: '#888', fontSize: '11px' }}>
|
<Typography variant="caption" sx={{ color: '#888', fontSize: '11px' }}>
|
||||||
PV {d.pvProduction.toFixed(1)} | Load {d.loadConsumption.toFixed(1)} | Grid {d.gridImport.toFixed(1)} kWh
|
PV {d.pvProduction.toFixed(1)} | Load {d.loadConsumption.toFixed(1)} | Grid {d.gridImport.toFixed(1)} kWh
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@ import {
|
||||||
I_UserWithInheritedAccess,
|
I_UserWithInheritedAccess,
|
||||||
InnovEnergyUser
|
InnovEnergyUser
|
||||||
} from '../interfaces/UserTypes';
|
} from '../interfaces/UserTypes';
|
||||||
import { FormattedMessage } from 'react-intl';
|
import { useIntl } from 'react-intl';
|
||||||
import { I_Installation } from '../interfaces/InstallationTypes';
|
import { I_Installation } from '../interfaces/InstallationTypes';
|
||||||
|
|
||||||
interface AccessContextProviderProps {
|
interface AccessContextProviderProps {
|
||||||
|
|
@ -69,10 +69,11 @@ export const AccessContext = createContext<AccessContextProviderProps>({
|
||||||
});
|
});
|
||||||
|
|
||||||
const AccessContextProvider = ({ children }: { children: ReactNode }) => {
|
const AccessContextProvider = ({ children }: { children: ReactNode }) => {
|
||||||
|
const intl = useIntl();
|
||||||
const [error, setError] = useState(false);
|
const [error, setError] = useState(false);
|
||||||
const [errormessage, setErrorMessage] = useState('An error has occured');
|
const [errormessage, setErrorMessage] = useState('');
|
||||||
const [updated, setUpdated] = useState(false);
|
const [updated, setUpdated] = useState(false);
|
||||||
const [updatedmessage, setUpdatedMessage] = useState('Successfully updated');
|
const [updatedmessage, setUpdatedMessage] = useState('');
|
||||||
const tokencontext = useContext(TokenContext);
|
const tokencontext = useContext(TokenContext);
|
||||||
const { removeToken } = tokencontext;
|
const { removeToken } = tokencontext;
|
||||||
const [usersWithDirectAccess, setUsersWithDirectAccess] = useState<
|
const [usersWithDirectAccess, setUsersWithDirectAccess] = useState<
|
||||||
|
|
@ -95,20 +96,12 @@ const AccessContextProvider = ({ children }: { children: ReactNode }) => {
|
||||||
setUsersWithDirectAccess(response.data);
|
setUsersWithDirectAccess(response.data);
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch(() => {
|
||||||
setError(true);
|
setError(true);
|
||||||
|
setErrorMessage(intl.formatMessage({ id: 'unableToLoadData' }));
|
||||||
const message = (
|
|
||||||
<FormattedMessage
|
|
||||||
id="unableToLoadData"
|
|
||||||
defaultMessage="Unable to load data"
|
|
||||||
/>
|
|
||||||
).props.defaultMessage;
|
|
||||||
|
|
||||||
setErrorMessage(message);
|
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
[]
|
[intl]
|
||||||
);
|
);
|
||||||
|
|
||||||
const fetchInstallationsForUser = useCallback(async (userId: number) => {
|
const fetchInstallationsForUser = useCallback(async (userId: number) => {
|
||||||
|
|
@ -119,17 +112,11 @@ const AccessContextProvider = ({ children }: { children: ReactNode }) => {
|
||||||
setAccessibleInstallationsForUser(response.data);
|
setAccessibleInstallationsForUser(response.data);
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch(() => {
|
||||||
setError(true);
|
setError(true);
|
||||||
const message = (
|
setErrorMessage(intl.formatMessage({ id: 'unableToLoadData' }));
|
||||||
<FormattedMessage
|
|
||||||
id="unableToLoadData"
|
|
||||||
defaultMessage="Unable to load data"
|
|
||||||
/>
|
|
||||||
).props.defaultMessage;
|
|
||||||
setErrorMessage(message);
|
|
||||||
});
|
});
|
||||||
}, []);
|
}, [intl]);
|
||||||
|
|
||||||
const fetchUsersWithInheritedAccessForResource = useCallback(
|
const fetchUsersWithInheritedAccessForResource = useCallback(
|
||||||
async (tempresourceType: string, id: number) => {
|
async (tempresourceType: string, id: number) => {
|
||||||
|
|
@ -140,18 +127,12 @@ const AccessContextProvider = ({ children }: { children: ReactNode }) => {
|
||||||
setUsersWithInheritedAccess(response.data);
|
setUsersWithInheritedAccess(response.data);
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch(() => {
|
||||||
setError(true);
|
setError(true);
|
||||||
const message = (
|
setErrorMessage(intl.formatMessage({ id: 'unableToLoadData' }));
|
||||||
<FormattedMessage
|
|
||||||
id="unableToLoadData"
|
|
||||||
defaultMessage="Unable to load data"
|
|
||||||
/>
|
|
||||||
).props.defaultMessage;
|
|
||||||
setErrorMessage(message);
|
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
[]
|
[intl]
|
||||||
);
|
);
|
||||||
|
|
||||||
const fetchAvailableUsers = async (): Promise<void> => {
|
const fetchAvailableUsers = async (): Promise<void> => {
|
||||||
|
|
@ -183,17 +164,7 @@ const AccessContextProvider = ({ children }: { children: ReactNode }) => {
|
||||||
current_ResourceId
|
current_ResourceId
|
||||||
);
|
);
|
||||||
|
|
||||||
const message =
|
setUpdatedMessage(intl.formatMessage({ id: 'revokedAccessFromUser' }) + ' ' + name);
|
||||||
(
|
|
||||||
<FormattedMessage
|
|
||||||
id="revokedAccessFromUser"
|
|
||||||
defaultMessage="Revoked access from user: "
|
|
||||||
/>
|
|
||||||
).props.defaultMessage +
|
|
||||||
' ' +
|
|
||||||
name;
|
|
||||||
|
|
||||||
setUpdatedMessage(message);
|
|
||||||
|
|
||||||
setUpdated(true);
|
setUpdated(true);
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
|
|
@ -201,19 +172,12 @@ const AccessContextProvider = ({ children }: { children: ReactNode }) => {
|
||||||
}, 3000);
|
}, 3000);
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch(() => {
|
||||||
setError(true);
|
setError(true);
|
||||||
const message = (
|
setErrorMessage(intl.formatMessage({ id: 'unableToRevokeAccess' }));
|
||||||
<FormattedMessage
|
|
||||||
id="unableToRevokeAccess"
|
|
||||||
defaultMessage="Unable to revoke access"
|
|
||||||
/>
|
|
||||||
).props.defaultMessage;
|
|
||||||
|
|
||||||
setErrorMessage(message);
|
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
[]
|
[intl, fetchUsersWithDirectAccessForResource, fetchUsersWithInheritedAccessForResource]
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,9 @@
|
||||||
"english": "Englisch",
|
"english": "Englisch",
|
||||||
"error": "Fehler",
|
"error": "Fehler",
|
||||||
"folder": "Ordner",
|
"folder": "Ordner",
|
||||||
|
"french": "Französisch",
|
||||||
"german": "Deutsch",
|
"german": "Deutsch",
|
||||||
|
"italian": "Italienisch",
|
||||||
"groupTabs": "Gruppen",
|
"groupTabs": "Gruppen",
|
||||||
"groupTree": "Gruppenbaum",
|
"groupTree": "Gruppenbaum",
|
||||||
"overview": "Überblick",
|
"overview": "Überblick",
|
||||||
|
|
@ -89,5 +91,44 @@
|
||||||
"unableToGrantAccess": "Der Zugriff kann nicht gewährt werden",
|
"unableToGrantAccess": "Der Zugriff kann nicht gewährt werden",
|
||||||
"unableToLoadData": "Daten können nicht geladen werden",
|
"unableToLoadData": "Daten können nicht geladen werden",
|
||||||
"unableToRevokeAccess": "Der Zugriff konnte nicht widerrufen werden",
|
"unableToRevokeAccess": "Der Zugriff konnte nicht widerrufen werden",
|
||||||
"revokedAccessFromUser": "Zugriff vom Benutzer widerrufen"
|
"revokedAccessFromUser": "Zugriff vom Benutzer widerrufen",
|
||||||
|
"Show Errors": "Fehler anzeigen",
|
||||||
|
"Show Warnings": "Warnungen anzeigen",
|
||||||
|
"lastSeen": "Zuletzt gesehen",
|
||||||
|
"reportTitle": "Wöchentlicher Leistungsbericht",
|
||||||
|
"weeklyInsights": "Wöchentliche Einblicke",
|
||||||
|
"weeklySavings": "Ihre Einsparungen diese Woche",
|
||||||
|
"solarEnergyUsed": "Genutzte Solarenergie",
|
||||||
|
"solarStayedHome": "Ihrer Solarenergie blieb zu Hause",
|
||||||
|
"estMoneySaved": "Geschätzte Ersparnisse",
|
||||||
|
"atCHFRate": "bei 0,27 CHF/kWh Ø",
|
||||||
|
"solarCoverage": "Solarabdeckung",
|
||||||
|
"fromSolarSub": "des Verbrauchs aus Solar",
|
||||||
|
"batteryEfficiency": "Batterieeffizienz",
|
||||||
|
"batteryEffSub": "Energie aus vs. Energie ein",
|
||||||
|
"weeklySummary": "Wöchentliche Zusammenfassung",
|
||||||
|
"metric": "Kennzahl",
|
||||||
|
"thisWeek": "Diese Woche",
|
||||||
|
"change": "Änderung",
|
||||||
|
"pvProduction": "PV-Produktion",
|
||||||
|
"consumption": "Verbrauch",
|
||||||
|
"gridImport": "Netzbezug",
|
||||||
|
"gridExport": "Netzeinspeisung",
|
||||||
|
"batteryInOut": "Batterie Ein / Aus",
|
||||||
|
"dailyBreakdown": "Tägliche Aufschlüsselung",
|
||||||
|
"prevWeek": "(Vorwoche)",
|
||||||
|
"sendReport": "Bericht senden",
|
||||||
|
"generatingReport": "Wochenbericht wird erstellt...",
|
||||||
|
"reportSentTo": "Bericht gesendet an {email}",
|
||||||
|
"reportSendError": "Senden fehlgeschlagen. Bitte überprüfen Sie die E-Mail-Adresse und versuchen Sie es erneut.",
|
||||||
|
"ok": "Ok",
|
||||||
|
"grantedAccessToUser": "Zugriff für Benutzer {name} gewährt",
|
||||||
|
"proceed": "Fortfahren",
|
||||||
|
"firmwareUpdating": "Firmware wird aktualisiert. Bitte warten...",
|
||||||
|
"confirmFirmwareUpdate": "Möchten Sie die Firmware wirklich aktualisieren?",
|
||||||
|
"batteryServiceStopWarning": "Diese Aktion erfordert, dass der Batteriedienst ca. 10-15 Minuten gestoppt wird.",
|
||||||
|
"downloadingBatteryLog": "Das Batterieprotokoll wird heruntergeladen. Es wird im Downloads-Ordner gespeichert. Bitte warten...",
|
||||||
|
"confirmBatteryLogDownload": "Möchten Sie das Batterieprotokoll wirklich herunterladen?",
|
||||||
|
"downloadBatteryLogFailed": "Herunterladen des Batterieprotokolls fehlgeschlagen, bitte versuchen Sie es erneut.",
|
||||||
|
"noReportData": "Keine Berichtsdaten gefunden."
|
||||||
}
|
}
|
||||||
|
|
@ -5,6 +5,9 @@
|
||||||
"customerName": "Customer name",
|
"customerName": "Customer name",
|
||||||
"english": "English",
|
"english": "English",
|
||||||
"german": "German",
|
"german": "German",
|
||||||
|
"french": "French",
|
||||||
|
"italian": "Italian",
|
||||||
|
"language": "Language",
|
||||||
"installation": "Installation",
|
"installation": "Installation",
|
||||||
"location": "Location",
|
"location": "Location",
|
||||||
"log": "Log",
|
"log": "Log",
|
||||||
|
|
@ -70,5 +73,44 @@
|
||||||
"unableToGrantAccess": "Unable to grant access to: ",
|
"unableToGrantAccess": "Unable to grant access to: ",
|
||||||
"unableToLoadData": "Unable to load data",
|
"unableToLoadData": "Unable to load data",
|
||||||
"unableToRevokeAccess": "Unable to revoke access",
|
"unableToRevokeAccess": "Unable to revoke access",
|
||||||
"revokedAccessFromUser": "Revoked access from user: "
|
"revokedAccessFromUser": "Revoked access from user: ",
|
||||||
|
"Show Errors": "Show Errors",
|
||||||
|
"Show Warnings": "Show Warnings",
|
||||||
|
"lastSeen": "Last seen",
|
||||||
|
"reportTitle": "Weekly Performance Report",
|
||||||
|
"weeklyInsights": "Weekly Insights",
|
||||||
|
"weeklySavings": "Your Savings This Week",
|
||||||
|
"solarEnergyUsed": "Solar Energy Used",
|
||||||
|
"solarStayedHome": "of your solar stayed at home",
|
||||||
|
"estMoneySaved": "Est. Money Saved",
|
||||||
|
"atCHFRate": "at 0.27 CHF/kWh avg.",
|
||||||
|
"solarCoverage": "Solar Coverage",
|
||||||
|
"fromSolarSub": "of consumption from solar",
|
||||||
|
"batteryEfficiency": "Battery Efficiency",
|
||||||
|
"batteryEffSub": "energy out vs energy in",
|
||||||
|
"weeklySummary": "Weekly Summary",
|
||||||
|
"metric": "Metric",
|
||||||
|
"thisWeek": "This Week",
|
||||||
|
"change": "Change",
|
||||||
|
"pvProduction": "PV Production",
|
||||||
|
"consumption": "Consumption",
|
||||||
|
"gridImport": "Grid Import",
|
||||||
|
"gridExport": "Grid Export",
|
||||||
|
"batteryInOut": "Battery In / Out",
|
||||||
|
"dailyBreakdown": "Daily Breakdown",
|
||||||
|
"prevWeek": "(prev week)",
|
||||||
|
"sendReport": "Send Report",
|
||||||
|
"generatingReport": "Generating weekly report...",
|
||||||
|
"reportSentTo": "Report sent to {email}",
|
||||||
|
"reportSendError": "Failed to send. Please check the email address and try again.",
|
||||||
|
"ok": "Ok",
|
||||||
|
"grantedAccessToUser": "Granted access to user {name}",
|
||||||
|
"proceed": "Proceed",
|
||||||
|
"firmwareUpdating": "The firmware is getting updated. Please wait...",
|
||||||
|
"confirmFirmwareUpdate": "Do you really want to update the firmware?",
|
||||||
|
"batteryServiceStopWarning": "This action requires the battery service to be stopped for around 10-15 minutes.",
|
||||||
|
"downloadingBatteryLog": "The battery log is getting downloaded. It will be saved in the Downloads folder. Please wait...",
|
||||||
|
"confirmBatteryLogDownload": "Do you really want to download battery log?",
|
||||||
|
"downloadBatteryLogFailed": "Download battery log failed, please try again.",
|
||||||
|
"noReportData": "No report data found."
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,10 @@
|
||||||
"english": "Anglais",
|
"english": "Anglais",
|
||||||
"error": "Erreur",
|
"error": "Erreur",
|
||||||
"folder": "Dossier",
|
"folder": "Dossier",
|
||||||
|
"french": "Français",
|
||||||
"german": "Allemand",
|
"german": "Allemand",
|
||||||
|
"italian": "Italien",
|
||||||
|
"language": "Langue",
|
||||||
"overview": "Aperçu",
|
"overview": "Aperçu",
|
||||||
"manage": "Gestion des accès",
|
"manage": "Gestion des accès",
|
||||||
"configuration": "Configuration",
|
"configuration": "Configuration",
|
||||||
|
|
@ -19,7 +22,6 @@
|
||||||
"apply_changes": "Appliquer",
|
"apply_changes": "Appliquer",
|
||||||
"delete_user": "Supprimer l'utilisateur",
|
"delete_user": "Supprimer l'utilisateur",
|
||||||
"installation_name_simple": "Nom de l'installation: ",
|
"installation_name_simple": "Nom de l'installation: ",
|
||||||
"language": "Langue",
|
|
||||||
"minimum_soc": "Soc minimum",
|
"minimum_soc": "Soc minimum",
|
||||||
"calibration_charge_forced": "Charge d'étalonnage forcée",
|
"calibration_charge_forced": "Charge d'étalonnage forcée",
|
||||||
"grid_set_point": "Point de consigne de grid",
|
"grid_set_point": "Point de consigne de grid",
|
||||||
|
|
@ -41,7 +43,7 @@
|
||||||
"lastWeek": "La semaine dernière",
|
"lastWeek": "La semaine dernière",
|
||||||
"location": "Localité",
|
"location": "Localité",
|
||||||
"log": "Journal",
|
"log": "Journal",
|
||||||
"logout": "Fermer las session",
|
"logout": "Fermer la session",
|
||||||
"makeASelection": "Veuillez faire une sélection à gauche",
|
"makeASelection": "Veuillez faire une sélection à gauche",
|
||||||
"manageAccess": "Gérer l'accès",
|
"manageAccess": "Gérer l'accès",
|
||||||
"move": "Déplacer",
|
"move": "Déplacer",
|
||||||
|
|
@ -63,7 +65,7 @@
|
||||||
"status": "Statut",
|
"status": "Statut",
|
||||||
"live": "Diffusion en direct",
|
"live": "Diffusion en direct",
|
||||||
"deleteInstallation": "Supprimer l'installation",
|
"deleteInstallation": "Supprimer l'installation",
|
||||||
"errorOccured": "Une erreur s’est produite",
|
"errorOccured": "Une erreur s'est produite",
|
||||||
"successfullyUpdated": "Mise à jour réussie",
|
"successfullyUpdated": "Mise à jour réussie",
|
||||||
"grantAccess": "Accorder l'accès",
|
"grantAccess": "Accorder l'accès",
|
||||||
"UserswithDirectAccess": "Utilisateurs avec accès direct",
|
"UserswithDirectAccess": "Utilisateurs avec accès direct",
|
||||||
|
|
@ -83,5 +85,44 @@
|
||||||
"unableToGrantAccess": "Impossible d'accorder l'accès à",
|
"unableToGrantAccess": "Impossible d'accorder l'accès à",
|
||||||
"unableToLoadData": "Impossible de charger les données",
|
"unableToLoadData": "Impossible de charger les données",
|
||||||
"unableToRevokeAccess": "Impossible de révoquer l'accès",
|
"unableToRevokeAccess": "Impossible de révoquer l'accès",
|
||||||
"revokedAccessFromUser": "Accès révoqué de l'utilisateur"
|
"revokedAccessFromUser": "Accès révoqué de l'utilisateur",
|
||||||
|
"Show Errors": "Afficher les erreurs",
|
||||||
|
"Show Warnings": "Afficher les avertissements",
|
||||||
|
"lastSeen": "Dernière connexion",
|
||||||
|
"reportTitle": "Rapport de performance hebdomadaire",
|
||||||
|
"weeklyInsights": "Aperçus hebdomadaires",
|
||||||
|
"weeklySavings": "Vos économies cette semaine",
|
||||||
|
"solarEnergyUsed": "Énergie solaire utilisée",
|
||||||
|
"solarStayedHome": "de votre solaire est resté à la maison",
|
||||||
|
"estMoneySaved": "Économies estimées",
|
||||||
|
"atCHFRate": "à 0,27 CHF/kWh moy.",
|
||||||
|
"solarCoverage": "Couverture solaire",
|
||||||
|
"fromSolarSub": "de la consommation provenant du solaire",
|
||||||
|
"batteryEfficiency": "Efficacité de la batterie",
|
||||||
|
"batteryEffSub": "énergie sortante vs énergie entrante",
|
||||||
|
"weeklySummary": "Résumé hebdomadaire",
|
||||||
|
"metric": "Métrique",
|
||||||
|
"thisWeek": "Cette semaine",
|
||||||
|
"change": "Variation",
|
||||||
|
"pvProduction": "Production PV",
|
||||||
|
"consumption": "Consommation",
|
||||||
|
"gridImport": "Importation réseau",
|
||||||
|
"gridExport": "Exportation réseau",
|
||||||
|
"batteryInOut": "Batterie Entrée / Sortie",
|
||||||
|
"dailyBreakdown": "Répartition quotidienne",
|
||||||
|
"prevWeek": "(semaine précédente)",
|
||||||
|
"sendReport": "Envoyer le rapport",
|
||||||
|
"generatingReport": "Génération du rapport hebdomadaire...",
|
||||||
|
"reportSentTo": "Rapport envoyé à {email}",
|
||||||
|
"reportSendError": "Échec de l'envoi. Veuillez vérifier l'adresse e-mail et réessayer.",
|
||||||
|
"ok": "Ok",
|
||||||
|
"grantedAccessToUser": "Accès accordé à l'utilisateur {name}",
|
||||||
|
"proceed": "Continuer",
|
||||||
|
"firmwareUpdating": "Le firmware est en cours de mise à jour. Veuillez patienter...",
|
||||||
|
"confirmFirmwareUpdate": "Voulez-vous vraiment mettre à jour le firmware?",
|
||||||
|
"batteryServiceStopWarning": "Cette action nécessite l'arrêt du service batterie pendant environ 10-15 minutes.",
|
||||||
|
"downloadingBatteryLog": "Le journal de la batterie est en cours de téléchargement. Il sera enregistré dans le dossier Téléchargements. Veuillez patienter...",
|
||||||
|
"confirmBatteryLogDownload": "Voulez-vous vraiment télécharger le journal de la batterie?",
|
||||||
|
"downloadBatteryLogFailed": "Échec du téléchargement du journal de la batterie, veuillez réessayer.",
|
||||||
|
"noReportData": "Aucune donnée de rapport trouvée."
|
||||||
}
|
}
|
||||||
|
|
@ -0,0 +1,139 @@
|
||||||
|
{
|
||||||
|
"allInstallations": "Tutte le installazioni",
|
||||||
|
"applyChanges": "Applica modifiche",
|
||||||
|
"country": "Paese",
|
||||||
|
"customerName": "Nome cliente",
|
||||||
|
"english": "Inglese",
|
||||||
|
"german": "Tedesco",
|
||||||
|
"french": "Francese",
|
||||||
|
"italian": "Italiano",
|
||||||
|
"language": "Lingua",
|
||||||
|
"installation": "Installazione",
|
||||||
|
"location": "Posizione",
|
||||||
|
"log": "Registro",
|
||||||
|
"orderNumbers": "Numeri d'ordine",
|
||||||
|
"region": "Regione",
|
||||||
|
"search": "Cerca",
|
||||||
|
"users": "Utenti",
|
||||||
|
"logout": "Disconnetti",
|
||||||
|
"updatedSuccessfully": "Aggiornamento riuscito",
|
||||||
|
"groups": "Gruppi",
|
||||||
|
"group": "Gruppo",
|
||||||
|
"folder": "Cartella",
|
||||||
|
"updateFolderErrorMessage": "Impossibile aggiornare la cartella, si è verificato un errore",
|
||||||
|
"Information": "Informazioni",
|
||||||
|
"addNewChild": "Aggiungi nuovo figlio",
|
||||||
|
"addNewDialogButton": "Aggiungi nuovo pulsante di dialogo",
|
||||||
|
"addUser": "Crea utente",
|
||||||
|
"createNewFolder": "Crea nuova cartella",
|
||||||
|
"createNewUser": "Crea nuovo utente",
|
||||||
|
"email": "Email",
|
||||||
|
"error": "Errore",
|
||||||
|
"groupTabs": "Schede gruppo",
|
||||||
|
"groupTree": "Albero gruppo",
|
||||||
|
"information": "Informazioni",
|
||||||
|
"inheritedAccess": "Accesso ereditato da",
|
||||||
|
"installationTabs": "Schede installazione",
|
||||||
|
"installations": "Installazioni",
|
||||||
|
"lastWeek": "Settimana scorsa",
|
||||||
|
"makeASelection": "Effettuare una selezione a sinistra",
|
||||||
|
"manageAccess": "Gestisci accesso",
|
||||||
|
"move": "Sposta",
|
||||||
|
"moveTo": "Sposta in",
|
||||||
|
"moveTree": "Sposta albero",
|
||||||
|
"name": "Nome",
|
||||||
|
"navigationTabs": "Schede di navigazione",
|
||||||
|
"requiredLocation": "La posizione è obbligatoria",
|
||||||
|
"requiredName": "Il nome è obbligatorio",
|
||||||
|
"requiredRegion": "La regione è obbligatoria",
|
||||||
|
"requiredOrderNumber": "Numero d'ordine obbligatorio",
|
||||||
|
"submit": "Invia",
|
||||||
|
"user": "Utente",
|
||||||
|
"userTabs": "Schede utente",
|
||||||
|
"status": "Stato",
|
||||||
|
"live": "Vista in diretta",
|
||||||
|
"deleteInstallation": "Elimina installazione",
|
||||||
|
"errorOccured": "Si è verificato un errore",
|
||||||
|
"successfullyUpdated": "Aggiornamento riuscito",
|
||||||
|
"grantAccess": "Concedi accesso",
|
||||||
|
"UserswithDirectAccess": "Utenti con accesso diretto",
|
||||||
|
"UserswithInheritedAccess": "Utenti con accesso ereditato",
|
||||||
|
"noerrors": "Non ci sono errori",
|
||||||
|
"nowarnings": "Non ci sono avvisi",
|
||||||
|
"noUsersWithDirectAccessToThis": "Nessun utente con accesso diretto a questo",
|
||||||
|
"selectUsers": "Seleziona utenti",
|
||||||
|
"cancel": "Annulla",
|
||||||
|
"addNewFolder": "Aggiungi nuova cartella",
|
||||||
|
"addNewInstallation": "Aggiungi nuova installazione",
|
||||||
|
"deleteFolder": "Elimina cartella",
|
||||||
|
"grantAccessToFolders": "Concedi accesso alle cartelle",
|
||||||
|
"grantAccessToInstallations": "Concedi accesso alle installazioni",
|
||||||
|
"cannotloadloggingdata": "Impossibile caricare i dati di registro",
|
||||||
|
"grantedAccessToUsers": "Accesso concesso agli utenti: ",
|
||||||
|
"unableToGrantAccess": "Impossibile concedere l'accesso a: ",
|
||||||
|
"unableToLoadData": "Impossibile caricare i dati",
|
||||||
|
"unableToRevokeAccess": "Impossibile revocare l'accesso",
|
||||||
|
"revokedAccessFromUser": "Accesso revocato all'utente: ",
|
||||||
|
"alarms": "Allarmi",
|
||||||
|
"overview": "Panoramica",
|
||||||
|
"manage": "Gestione accessi",
|
||||||
|
"configuration": "Configurazione",
|
||||||
|
"installation_name_simple": "Nome installazione: ",
|
||||||
|
"installation_name": "Nome installazione",
|
||||||
|
"minimum_soc": "SoC minimo",
|
||||||
|
"calibration_charge_forced": "Carica di calibrazione forzata",
|
||||||
|
"grid_set_point": "Punto di riferimento rete",
|
||||||
|
"Installed_Power_DC1010": "Potenza installata DC1010",
|
||||||
|
"Maximum_Discharge_Power": "Potenza massima di scarica",
|
||||||
|
"Number_of_Batteries": "Numero di batterie",
|
||||||
|
"24_hours": "24 ore",
|
||||||
|
"lastweek": "Settimana scorsa",
|
||||||
|
"lastmonth": "Mese scorso",
|
||||||
|
"apply_changes": "Applica modifiche",
|
||||||
|
"delete_user": "Elimina utente",
|
||||||
|
"battery_temperature": "Temperatura batteria",
|
||||||
|
"pv_production": "Produzione fotovoltaica",
|
||||||
|
"grid_power": "Potenza di rete",
|
||||||
|
"battery_power": "Potenza batteria",
|
||||||
|
"dc_voltage": "Tensione bus DC",
|
||||||
|
"battery_soc": "Stato di carica (SOC)",
|
||||||
|
"Show Errors": "Mostra errori",
|
||||||
|
"Show Warnings": "Mostra avvisi",
|
||||||
|
"lastSeen": "Ultima visualizzazione",
|
||||||
|
"reportTitle": "Rapporto settimanale sulle prestazioni",
|
||||||
|
"weeklyInsights": "Approfondimenti settimanali",
|
||||||
|
"weeklySavings": "I tuoi risparmi questa settimana",
|
||||||
|
"solarEnergyUsed": "Energia solare utilizzata",
|
||||||
|
"solarStayedHome": "della tua energia solare è rimasta a casa",
|
||||||
|
"estMoneySaved": "Risparmio stimato",
|
||||||
|
"atCHFRate": "a 0,27 CHF/kWh media",
|
||||||
|
"solarCoverage": "Copertura solare",
|
||||||
|
"fromSolarSub": "del consumo da fonte solare",
|
||||||
|
"batteryEfficiency": "Efficienza della batteria",
|
||||||
|
"batteryEffSub": "energia in uscita vs energia in entrata",
|
||||||
|
"weeklySummary": "Riepilogo settimanale",
|
||||||
|
"metric": "Metrica",
|
||||||
|
"thisWeek": "Questa settimana",
|
||||||
|
"change": "Variazione",
|
||||||
|
"pvProduction": "Produzione FV",
|
||||||
|
"consumption": "Consumo",
|
||||||
|
"gridImport": "Importazione rete",
|
||||||
|
"gridExport": "Esportazione rete",
|
||||||
|
"batteryInOut": "Batteria Entrata / Uscita",
|
||||||
|
"dailyBreakdown": "Ripartizione giornaliera",
|
||||||
|
"prevWeek": "(settimana precedente)",
|
||||||
|
"sendReport": "Invia rapporto",
|
||||||
|
"generatingReport": "Generazione del rapporto settimanale...",
|
||||||
|
"reportSentTo": "Rapporto inviato a {email}",
|
||||||
|
"reportSendError": "Invio fallito. Verificare l'indirizzo e-mail e riprovare.",
|
||||||
|
"ok": "Ok",
|
||||||
|
"grantedAccessToUser": "Accesso concesso all'utente {name}",
|
||||||
|
"proceed": "Procedi",
|
||||||
|
"firmwareUpdating": "Il firmware è in fase di aggiornamento. Attendere prego...",
|
||||||
|
"confirmFirmwareUpdate": "Vuoi davvero aggiornare il firmware?",
|
||||||
|
"batteryServiceStopWarning": "Questa azione richiede l'interruzione del servizio batteria per circa 10-15 minuti.",
|
||||||
|
"downloadingBatteryLog": "Il registro della batteria è in fase di download. Verrà salvato nella cartella Download. Attendere prego...",
|
||||||
|
"confirmBatteryLogDownload": "Vuoi davvero scaricare il registro della batteria?",
|
||||||
|
"downloadBatteryLogFailed": "Download del registro della batteria fallito, riprovare.",
|
||||||
|
"noReportData": "Nessun dato del rapporto trovato."
|
||||||
|
}
|
||||||
|
|
@ -141,10 +141,13 @@ function HeaderMenu(props: HeaderButtonsProps) {
|
||||||
English
|
English
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
<MenuItem value="de" onClick={() => handleLanguageSelect('de')}>
|
<MenuItem value="de" onClick={() => handleLanguageSelect('de')}>
|
||||||
German
|
Deutsch
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
<MenuItem value="fr" onClick={() => handleLanguageSelect('fr')}>
|
<MenuItem value="fr" onClick={() => handleLanguageSelect('fr')}>
|
||||||
French
|
Français
|
||||||
|
</MenuItem>
|
||||||
|
<MenuItem value="it" onClick={() => handleLanguageSelect('it')}>
|
||||||
|
Italiano
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
</Menu>
|
</Menu>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue