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)
|
||||
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)
|
||||
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.
|
||||
/// </summary>
|
||||
[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;
|
||||
if (user == null)
|
||||
|
|
@ -885,7 +885,8 @@ public class Controller : ControllerBase
|
|||
|
||||
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);
|
||||
}
|
||||
catch (Exception ex)
|
||||
|
|
@ -911,8 +912,9 @@ public class Controller : ControllerBase
|
|||
|
||||
try
|
||||
{
|
||||
var report = await WeeklyReportService.GenerateReportAsync(installationId, installation.InstallationName);
|
||||
await ReportEmailService.SendReportEmailAsync(report, emailAddress);
|
||||
var lang = user.Language ?? "en";
|
||||
var report = await WeeklyReportService.GenerateReportAsync(installationId, installation.InstallationName, lang);
|
||||
await ReportEmailService.SendReportEmailAsync(report, emailAddress, lang);
|
||||
return Ok(new { message = $"Report sent to {emailAddress}" });
|
||||
}
|
||||
catch (Exception ex)
|
||||
|
|
|
|||
|
|
@ -237,26 +237,63 @@ public static class UserMethods
|
|||
|
||||
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
|
||||
var encodedToken = HttpUtility.UrlEncode(token);
|
||||
|
||||
var body = $"Dear {user.Name}\n" +
|
||||
$"To reset your password " +
|
||||
$"please open this link:{resetLink}?token={encodedToken}";
|
||||
var (subject, body) = (user.Language ?? "en") switch
|
||||
{
|
||||
"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);
|
||||
}
|
||||
|
||||
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 body = $"Dear {user.Name}\n" +
|
||||
$"To set your password and log in to your " +
|
||||
$"Inesco Energy Account open this link:{resetLink}";
|
||||
var (subject, body) = (user.Language ?? "en") switch
|
||||
{
|
||||
"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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ public class User : TreeNode
|
|||
public int UserType { get; set; } = 0;
|
||||
public Boolean MustResetPassword { get; set; } = false;
|
||||
public String? Password { get; set; } = null!;
|
||||
public String Language { get; set; } = "en";
|
||||
|
||||
[Unique]
|
||||
public override String Name { get; set; } = null!;
|
||||
|
|
|
|||
|
|
@ -53,6 +53,12 @@ public static partial class Db
|
|||
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();
|
||||
CleanupSessions().SupressAwaitWarning();
|
||||
DeleteSnapshots().SupressAwaitWarning();
|
||||
|
|
|
|||
|
|
@ -38,26 +38,40 @@ public static class DiagnosticService
|
|||
|
||||
// ── public entry-point ──────────────────────────────────────────
|
||||
|
||||
/// <summary>
|
||||
/// Returns a diagnosis for <paramref name="errorDescription"/>.
|
||||
/// First checks the static AlarmKnowledgeBase for known Sinexcel/Growatt alarms.
|
||||
/// Falls back to in-memory cache, then calls Mistral AI only for unknown alarms.
|
||||
/// </summary>
|
||||
public static async Task<DiagnosticResponse?> DiagnoseAsync(Int64 installationId, string errorDescription)
|
||||
private static string LanguageName(string code) => code switch
|
||||
{
|
||||
// 1. Check the static knowledge base first (no API call needed)
|
||||
var knownDiagnosis = AlarmKnowledgeBase.TryGetDiagnosis(errorDescription);
|
||||
if (knownDiagnosis is not null)
|
||||
"de" => "German",
|
||||
"fr" => "French",
|
||||
"it" => "Italian",
|
||||
_ => "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")
|
||||
{
|
||||
Console.WriteLine($"[DiagnosticService] Found diagnosis in knowledge base for: {errorDescription}");
|
||||
return knownDiagnosis;
|
||||
var knownDiagnosis = AlarmKnowledgeBase.TryGetDiagnosis(errorDescription);
|
||||
if (knownDiagnosis is not null)
|
||||
{
|
||||
Console.WriteLine($"[DiagnosticService] Found diagnosis in knowledge base for: {errorDescription}");
|
||||
return knownDiagnosis;
|
||||
}
|
||||
}
|
||||
|
||||
// 2. If AI is not enabled, we can't proceed further
|
||||
if (!IsEnabled) return null;
|
||||
|
||||
// 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;
|
||||
|
||||
// 4. Gather context from the DB for AI prompt
|
||||
|
|
@ -77,14 +91,14 @@ public static class DiagnosticService
|
|||
.ToList();
|
||||
|
||||
// 5. Build prompt and call Mistral API (only for unknown alarms)
|
||||
Console.WriteLine($"[DiagnosticService] Calling Mistral for unknown alarm: {errorDescription}");
|
||||
var prompt = BuildPrompt(errorDescription, productName, recentDescriptions);
|
||||
Console.WriteLine($"[DiagnosticService] Calling Mistral for unknown alarm: {errorDescription} ({language})");
|
||||
var prompt = BuildPrompt(errorDescription, productName, recentDescriptions, language);
|
||||
var response = await CallMistralAsync(prompt);
|
||||
|
||||
if (response is null) return null;
|
||||
|
||||
// 6. Store in cache for future requests
|
||||
Cache.TryAdd(errorDescription, response);
|
||||
Cache.TryAdd(cacheKey, response);
|
||||
return response;
|
||||
}
|
||||
|
||||
|
|
@ -101,7 +115,7 @@ public static class DiagnosticService
|
|||
if (Cache.TryGetValue(errorDescription, out var 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);
|
||||
|
||||
if (response is not null)
|
||||
|
|
@ -112,7 +126,7 @@ public static class DiagnosticService
|
|||
|
||||
// ── 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
|
||||
? 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
|
||||
- causes: 2-3 bullet points, plain language
|
||||
- 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"":[""...""]}}
|
||||
";
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,13 +10,14 @@ namespace InnovEnergy.App.Backend.Services;
|
|||
public static class ReportEmailService
|
||||
{
|
||||
/// <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.
|
||||
/// </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 html = BuildHtmlEmail(report);
|
||||
var strings = GetStrings(language);
|
||||
var subject = $"{strings.Title} — {report.InstallationName} ({report.PeriodStart} to {report.PeriodEnd})";
|
||||
var html = BuildHtmlEmail(report, strings);
|
||||
|
||||
var config = await ReadMailerConfig();
|
||||
|
||||
|
|
@ -49,9 +50,169 @@ public static class ReportEmailService
|
|||
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 ─────────────────────────────────────────────
|
||||
|
||||
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 prev = r.PreviousWeek;
|
||||
|
|
@ -91,47 +252,47 @@ public static class ReportEmailService
|
|||
var comparisonHtml = prev != null
|
||||
? $@"
|
||||
<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;color:#888"">{prev.TotalPvProduction:F1} kWh</td>
|
||||
<td style=""padding:8px 12px;border-bottom:1px solid #eee;text-align:right;color:{ChangeColor(r.PvChangePercent)}"">{FormatChange(r.PvChangePercent)}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style=""padding:8px 12px;border-bottom:1px solid #eee"">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;color:#888"">{prev.TotalConsumption:F1} kWh</td>
|
||||
<td style=""padding:8px 12px;border-bottom:1px solid #eee;text-align:right;color:{ChangeColor(-r.ConsumptionChangePercent)}"">{FormatChange(r.ConsumptionChangePercent)}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style=""padding:8px 12px;border-bottom:1px solid #eee"">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;color:#888"">{prev.TotalGridImport:F1} kWh</td>
|
||||
<td style=""padding:8px 12px;border-bottom:1px solid #eee;text-align:right;color:{ChangeColor(-r.GridImportChangePercent)}"">{FormatChange(r.GridImportChangePercent)}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style=""padding:8px 12px;border-bottom:1px solid #eee"">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;color:#888"">{prev.TotalGridExport:F1} kWh</td>
|
||||
<td style=""padding:8px 12px;border-bottom:1px solid #eee;text-align:right"">—</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style=""padding:8px 12px;border-bottom:1px solid #eee"">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;color:#888"">{prev.TotalBatteryCharged:F1}/{prev.TotalBatteryDischarged:F1} kWh</td>
|
||||
<td style=""padding:8px 12px;border-bottom:1px solid #eee;text-align:right"">—</td>
|
||||
</tr>"
|
||||
: $@"
|
||||
<tr><td style=""padding:8px 12px"">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"">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"">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"">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.PvProduction}</td><td style=""padding:8px 12px;text-align:right;font-weight:bold"">{cur.TotalPvProduction:F1} kWh</td></tr>
|
||||
<tr><td style=""padding:8px 12px"">{s.Consumption}</td><td style=""padding:8px 12px;text-align:right;font-weight:bold"">{cur.TotalConsumption:F1} kWh</td></tr>
|
||||
<tr><td style=""padding:8px 12px"">{s.GridImport}</td><td style=""padding:8px 12px;text-align:right;font-weight:bold"">{cur.TotalGridImport:F1} kWh</td></tr>
|
||||
<tr><td style=""padding:8px 12px"">{s.GridExport}</td><td style=""padding:8px 12px;text-align:right;font-weight:bold"">{cur.TotalGridExport:F1} kWh</td></tr>
|
||||
<tr><td style=""padding:8px 12px"">{s.BatteryInOut}</td><td style=""padding:8px 12px;text-align:right;font-weight:bold"">{cur.TotalBatteryCharged:F1}/{cur.TotalBatteryDischarged:F1} kWh</td></tr>";
|
||||
|
||||
var comparisonHeaders = prev != null
|
||||
? @"<th style=""padding:8px 12px;text-align:right"">This Week</th>
|
||||
<th style=""padding:8px 12px;text-align:right"">Last Week</th>
|
||||
<th style=""padding:8px 12px;text-align:right"">Change</th>"
|
||||
: @"<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"">{s.LastWeek}</th>
|
||||
<th style=""padding:8px 12px;text-align:right"">{s.Change}</th>"
|
||||
: $@"<th style=""padding:8px 12px;text-align:right"">{s.ThisWeek}</th>";
|
||||
|
||||
return $@"
|
||||
<!DOCTYPE html>
|
||||
|
|
@ -145,7 +306,7 @@ public static class ReportEmailService
|
|||
<!-- Header -->
|
||||
<tr>
|
||||
<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:13px;margin-top:2px;opacity:0.7"">{r.PeriodStart} — {r.PeriodEnd}</div>
|
||||
</td>
|
||||
|
|
@ -154,7 +315,7 @@ public static class ReportEmailService
|
|||
<!-- Weekly Insights (top) -->
|
||||
<tr>
|
||||
<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"">
|
||||
{insightHtml}
|
||||
</div>
|
||||
|
|
@ -164,10 +325,10 @@ public static class ReportEmailService
|
|||
<!-- Weekly Totals -->
|
||||
<tr>
|
||||
<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"">
|
||||
<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}
|
||||
</tr>
|
||||
{comparisonHtml}
|
||||
|
|
@ -178,13 +339,13 @@ public static class ReportEmailService
|
|||
<!-- Key Ratios -->
|
||||
<tr>
|
||||
<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"">
|
||||
<tr>
|
||||
{SavingsBox("Solar Energy Used", $"{r.CurrentWeek.TotalPvProduction - r.CurrentWeek.TotalGridExport:F1} kWh", "stayed at home", "#27ae60")}
|
||||
{SavingsBox("Est. Money Saved", $"~{(r.CurrentWeek.TotalPvProduction - r.CurrentWeek.TotalGridExport) * 0.27:F1} CHF", "at 0.27 CHF/kWh", "#2980b9")}
|
||||
{SavingsBox("Solar Coverage", $"{r.SelfSufficiencyPercent:F0}%", "from solar", "#8e44ad")}
|
||||
{SavingsBox("Battery Eff.", $"{r.BatteryEfficiencyPercent:F0}%", "out vs in", "#e67e22")}
|
||||
{SavingsBox(s.SolarEnergyUsed, $"{r.CurrentWeek.TotalPvProduction - r.CurrentWeek.TotalGridExport:F1} kWh", s.StayedAtHome, "#27ae60")}
|
||||
{SavingsBox(s.EstMoneySaved, $"~{(r.CurrentWeek.TotalPvProduction - r.CurrentWeek.TotalGridExport) * 0.27:F1} CHF", s.AtRate, "#2980b9")}
|
||||
{SavingsBox(s.SolarCoverage, $"{r.SelfSufficiencyPercent:F0}%", s.FromSolar, "#8e44ad")}
|
||||
{SavingsBox(s.BatteryEff, $"{r.BatteryEfficiencyPercent:F0}%", s.OutVsIn, "#e67e22")}
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
|
|
@ -193,15 +354,15 @@ public static class ReportEmailService
|
|||
<!-- Daily Breakdown -->
|
||||
<tr>
|
||||
<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"">
|
||||
<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"">Load</th>
|
||||
<th style=""padding:6px 10px;text-align:right"">Grid In</th>
|
||||
<th style=""padding:6px 10px;text-align:right"">Grid Out</th>
|
||||
<th style=""padding:6px 10px;text-align:right"">Batt In/Out</th>
|
||||
<th style=""padding:6px 10px;text-align:right"">{s.Load}</th>
|
||||
<th style=""padding:6px 10px;text-align:right"">{s.GridIn}</th>
|
||||
<th style=""padding:6px 10px;text-align:right"">{s.GridOut}</th>
|
||||
<th style=""padding:6px 10px;text-align:right"">{s.BattInOut}</th>
|
||||
</tr>
|
||||
{dailyRows}
|
||||
</table>
|
||||
|
|
@ -211,7 +372,7 @@ public static class ReportEmailService
|
|||
<!-- Footer -->
|
||||
<tr>
|
||||
<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>
|
||||
</tr>
|
||||
|
||||
|
|
|
|||
|
|
@ -11,14 +11,17 @@ public static class WeeklyReportService
|
|||
|
||||
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>
|
||||
/// 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>
|
||||
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 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
|
||||
if (File.Exists(cachePath) && File.Exists(xlsxPath))
|
||||
|
|
@ -33,7 +36,7 @@ public static class WeeklyReportService
|
|||
await File.ReadAllTextAsync(cachePath));
|
||||
if (cached != null)
|
||||
{
|
||||
Console.WriteLine($"[WeeklyReportService] Returning cached report for installation {installationId}.");
|
||||
Console.WriteLine($"[WeeklyReportService] Returning cached report for installation {installationId} ({language}).");
|
||||
return cached;
|
||||
}
|
||||
}
|
||||
|
|
@ -45,7 +48,7 @@ public static class WeeklyReportService
|
|||
}
|
||||
|
||||
var allDays = ExcelDataParser.Parse(xlsxPath);
|
||||
var report = await GenerateReportFromDataAsync(allDays, installationName);
|
||||
var report = await GenerateReportFromDataAsync(allDays, installationName, language);
|
||||
|
||||
// Write cache
|
||||
try
|
||||
|
|
@ -64,7 +67,7 @@ public static class WeeklyReportService
|
|||
/// Core report generation from daily data. Data-source agnostic.
|
||||
/// </summary>
|
||||
public static async Task<WeeklyReportResponse> GenerateReportFromDataAsync(
|
||||
List<DailyEnergyData> allDays, string installationName)
|
||||
List<DailyEnergyData> allDays, string installationName, string language = "en")
|
||||
{
|
||||
// Sort by date
|
||||
allDays = allDays.OrderBy(d => d.Date).ToList();
|
||||
|
|
@ -111,7 +114,7 @@ public static class WeeklyReportService
|
|||
|
||||
// AI insight
|
||||
var aiInsight = await GetAiInsightAsync(currentWeekDays, currentSummary, previousSummary,
|
||||
selfSufficiency, gridDependency, batteryEfficiency, installationName);
|
||||
selfSufficiency, gridDependency, batteryEfficiency, installationName, language);
|
||||
|
||||
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 string LanguageName(string code) => code switch
|
||||
{
|
||||
"de" => "German",
|
||||
"fr" => "French",
|
||||
"it" => "Italian",
|
||||
_ => "English"
|
||||
};
|
||||
|
||||
private static async Task<string> GetAiInsightAsync(
|
||||
List<DailyEnergyData> currentWeek,
|
||||
WeeklySummary current,
|
||||
|
|
@ -159,7 +170,8 @@ public static class WeeklyReportService
|
|||
double selfSufficiency,
|
||||
double gridDependency,
|
||||
double batteryEfficiency,
|
||||
string installationName)
|
||||
string installationName,
|
||||
string language = "en")
|
||||
{
|
||||
var apiKey = Environment.GetEnvironmentVariable("MISTRAL_API_KEY");
|
||||
if (string.IsNullOrWhiteSpace(apiKey))
|
||||
|
|
@ -168,8 +180,8 @@ public static class WeeklyReportService
|
|||
return "AI insight unavailable (API key not configured).";
|
||||
}
|
||||
|
||||
// Cache key: installation + period
|
||||
var cacheKey = $"{installationName}_{currentWeek.Last().Date}";
|
||||
// Cache key: installation + period + language
|
||||
var cacheKey = $"{installationName}_{currentWeek.Last().Date}_{language}";
|
||||
if (InsightCache.TryGetValue(cacheKey, out var 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.
|
||||
|
||||
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).
|
||||
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.
|
||||
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.
|
||||
IMPORTANT: Write your entire response in {LanguageName(language)}.
|
||||
|
||||
Daily data (kWh):
|
||||
{dayLines}
|
||||
|
|
|
|||
|
|
@ -1,21 +1,22 @@
|
|||
import { Navigate, Route, Routes, useNavigate } from 'react-router-dom';
|
||||
import { CssBaseline } from '@mui/material';
|
||||
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 Login from './components/login';
|
||||
import { IntlProvider } from 'react-intl';
|
||||
import en from './lang/en.json';
|
||||
import de from './lang/de.json';
|
||||
import fr from './lang/fr.json';
|
||||
import it from './lang/it.json';
|
||||
import SuspenseLoader from './components/SuspenseLoader';
|
||||
import axiosConfig, { axiosConfigWithoutToken } from './Resources/axiosConfig';
|
||||
import SidebarLayout from './layouts/SidebarLayout';
|
||||
import { TokenContext } from './contexts/tokenContext';
|
||||
import InstallationTabs from './content/dashboards/Installations/index';
|
||||
import routes from 'src/Resources/routes.json';
|
||||
import './App.css';
|
||||
import ForgotPassword from './components/ForgotPassword';
|
||||
import { axiosConfigWithoutToken } from './Resources/axiosConfig';
|
||||
import InstallationsContextProvider from './contexts/InstallationsContextProvider';
|
||||
import AccessContextProvider from './contexts/AccessContextProvider';
|
||||
import SalidomoInstallationTabs from './content/dashboards/SalidomoInstallations';
|
||||
|
|
@ -37,15 +38,38 @@ function App() {
|
|||
setAccessToSodistore
|
||||
} = 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 = () => {
|
||||
switch (language) {
|
||||
case 'en':
|
||||
return en;
|
||||
case 'de':
|
||||
return de;
|
||||
case 'fr':
|
||||
return fr;
|
||||
case 'de': return de;
|
||||
case 'fr': return fr;
|
||||
case 'it': return it;
|
||||
default: return en;
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -151,7 +175,7 @@ function App() {
|
|||
element={
|
||||
<SidebarLayout
|
||||
language={language}
|
||||
onSelectLanguage={setLanguage}
|
||||
onSelectLanguage={onSelectLanguage}
|
||||
/>
|
||||
}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import React, { useContext, useEffect, useState } from 'react';
|
||||
import { FormattedMessage, useIntl } from 'react-intl';
|
||||
import { I_S3Credentials } from '../../../interfaces/S3Types';
|
||||
import {
|
||||
Box,
|
||||
|
|
@ -36,6 +37,7 @@ function DetailedBatteryView(props: DetailedBatteryViewProps) {
|
|||
if (props.batteryData === null) {
|
||||
return null;
|
||||
}
|
||||
const intl = useIntl();
|
||||
const navigate = useNavigate();
|
||||
const [openModalFirmwareUpdate, setOpenModalFirmwareUpdate] = useState(false);
|
||||
const [openModalResultFirmwareUpdate, setOpenModalResultFirmwareUpdate] =
|
||||
|
|
@ -242,7 +244,7 @@ function DetailedBatteryView(props: DetailedBatteryViewProps) {
|
|||
}
|
||||
} catch (error) {
|
||||
console.error('Error:', error.message);
|
||||
setErrorMessage('Download battery log failed, please try again.');
|
||||
setErrorMessage(intl.formatMessage({ id: 'downloadBatteryLogFailed' }));
|
||||
setOpenModalError(true);
|
||||
} finally {
|
||||
setOpenModalStartDownloadBatteryLog(false);
|
||||
|
|
@ -282,7 +284,7 @@ function DetailedBatteryView(props: DetailedBatteryViewProps) {
|
|||
gutterBottom
|
||||
sx={{ fontWeight: 'bold' }}
|
||||
>
|
||||
The firmware is getting updated. Please wait...
|
||||
<FormattedMessage id="firmwareUpdating" defaultMessage="The firmware is getting updated. Please wait..." />
|
||||
</Typography>
|
||||
|
||||
<div
|
||||
|
|
@ -302,7 +304,7 @@ function DetailedBatteryView(props: DetailedBatteryViewProps) {
|
|||
}}
|
||||
onClick={firmwareModalResultHandleOk}
|
||||
>
|
||||
Ok
|
||||
<FormattedMessage id="ok" defaultMessage="Ok" />
|
||||
</Button>
|
||||
</div>
|
||||
</Box>
|
||||
|
|
@ -337,12 +339,11 @@ function DetailedBatteryView(props: DetailedBatteryViewProps) {
|
|||
gutterBottom
|
||||
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 variant="body1" gutterBottom>
|
||||
This action requires the battery service to be stopped for around
|
||||
10-15 minutes.
|
||||
<FormattedMessage id="batteryServiceStopWarning" defaultMessage="This action requires the battery service to be stopped for around 10-15 minutes." />
|
||||
</Typography>
|
||||
|
||||
<div
|
||||
|
|
@ -362,7 +363,7 @@ function DetailedBatteryView(props: DetailedBatteryViewProps) {
|
|||
}}
|
||||
onClick={FirmwareModalHandleProceed}
|
||||
>
|
||||
Proceed
|
||||
<FormattedMessage id="proceed" defaultMessage="Proceed" />
|
||||
</Button>
|
||||
<Button
|
||||
sx={{
|
||||
|
|
@ -375,7 +376,7 @@ function DetailedBatteryView(props: DetailedBatteryViewProps) {
|
|||
}}
|
||||
onClick={FirmwareModalHandleCancel}
|
||||
>
|
||||
Cancel
|
||||
<FormattedMessage id="cancel" defaultMessage="Cancel" />
|
||||
</Button>
|
||||
</div>
|
||||
</Box>
|
||||
|
|
@ -409,8 +410,7 @@ function DetailedBatteryView(props: DetailedBatteryViewProps) {
|
|||
gutterBottom
|
||||
sx={{ fontWeight: 'bold' }}
|
||||
>
|
||||
The battery log is getting downloaded. It will be saved in the
|
||||
Downloads folder. Please wait...
|
||||
<FormattedMessage id="downloadingBatteryLog" defaultMessage="The battery log is getting downloaded. It will be saved in the Downloads folder. Please wait..." />
|
||||
</Typography>
|
||||
|
||||
<div
|
||||
|
|
@ -430,7 +430,7 @@ function DetailedBatteryView(props: DetailedBatteryViewProps) {
|
|||
}}
|
||||
onClick={StartDownloadBatteryLogModalHandleOk}
|
||||
>
|
||||
Ok
|
||||
<FormattedMessage id="ok" defaultMessage="Ok" />
|
||||
</Button>
|
||||
</div>
|
||||
</Box>
|
||||
|
|
@ -465,12 +465,11 @@ function DetailedBatteryView(props: DetailedBatteryViewProps) {
|
|||
gutterBottom
|
||||
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 variant="body1" gutterBottom>
|
||||
This action requires the battery service to be stopped for around
|
||||
10-15 minutes.
|
||||
<FormattedMessage id="batteryServiceStopWarning" defaultMessage="This action requires the battery service to be stopped for around 10-15 minutes." />
|
||||
</Typography>
|
||||
|
||||
<div
|
||||
|
|
@ -490,7 +489,7 @@ function DetailedBatteryView(props: DetailedBatteryViewProps) {
|
|||
}}
|
||||
onClick={DownloadBatteryLogModalHandleProceed}
|
||||
>
|
||||
Proceed
|
||||
<FormattedMessage id="proceed" defaultMessage="Proceed" />
|
||||
</Button>
|
||||
<Button
|
||||
sx={{
|
||||
|
|
@ -503,7 +502,7 @@ function DetailedBatteryView(props: DetailedBatteryViewProps) {
|
|||
}}
|
||||
onClick={DownloadBatteryLogModalHandleCancel}
|
||||
>
|
||||
Cancel
|
||||
<FormattedMessage id="cancel" defaultMessage="Cancel" />
|
||||
</Button>
|
||||
</div>
|
||||
</Box>
|
||||
|
|
@ -553,7 +552,7 @@ function DetailedBatteryView(props: DetailedBatteryViewProps) {
|
|||
}}
|
||||
onClick={ErrorModalHandleOk}
|
||||
>
|
||||
Ok
|
||||
<FormattedMessage id="ok" defaultMessage="Ok" />
|
||||
</Button>
|
||||
</div>
|
||||
</Box>
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import React, { useContext, useEffect, useState } from 'react';
|
||||
import { FormattedMessage, useIntl } from 'react-intl';
|
||||
import { I_S3Credentials } from '../../../interfaces/S3Types';
|
||||
import {
|
||||
Box,
|
||||
|
|
@ -36,7 +37,7 @@ function DetailedBatteryViewSalidomo(props: DetailedBatteryViewProps) {
|
|||
if (props.batteryData === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const intl = useIntl();
|
||||
const navigate = useNavigate();
|
||||
const [openModalFirmwareUpdate, setOpenModalFirmwareUpdate] = useState(false);
|
||||
const [openModalResultFirmwareUpdate, setOpenModalResultFirmwareUpdate] =
|
||||
|
|
@ -243,7 +244,7 @@ function DetailedBatteryViewSalidomo(props: DetailedBatteryViewProps) {
|
|||
}
|
||||
} catch (error) {
|
||||
console.error('Error:', error.message);
|
||||
setErrorMessage('Download battery log failed, please try again.');
|
||||
setErrorMessage(intl.formatMessage({ id: 'downloadBatteryLogFailed' }));
|
||||
setOpenModalError(true);
|
||||
} finally {
|
||||
setOpenModalStartDownloadBatteryLog(false);
|
||||
|
|
@ -283,7 +284,7 @@ function DetailedBatteryViewSalidomo(props: DetailedBatteryViewProps) {
|
|||
gutterBottom
|
||||
sx={{ fontWeight: 'bold' }}
|
||||
>
|
||||
The firmware is getting updated. Please wait...
|
||||
<FormattedMessage id="firmwareUpdating" defaultMessage="The firmware is getting updated. Please wait..." />
|
||||
</Typography>
|
||||
|
||||
<div
|
||||
|
|
@ -303,7 +304,7 @@ function DetailedBatteryViewSalidomo(props: DetailedBatteryViewProps) {
|
|||
}}
|
||||
onClick={firmwareModalResultHandleOk}
|
||||
>
|
||||
Ok
|
||||
<FormattedMessage id="ok" defaultMessage="Ok" />
|
||||
</Button>
|
||||
</div>
|
||||
</Box>
|
||||
|
|
@ -338,12 +339,11 @@ function DetailedBatteryViewSalidomo(props: DetailedBatteryViewProps) {
|
|||
gutterBottom
|
||||
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 variant="body1" gutterBottom>
|
||||
This action requires the battery service to be stopped for around
|
||||
10-15 minutes.
|
||||
<FormattedMessage id="batteryServiceStopWarning" defaultMessage="This action requires the battery service to be stopped for around 10-15 minutes." />
|
||||
</Typography>
|
||||
|
||||
<div
|
||||
|
|
@ -363,7 +363,7 @@ function DetailedBatteryViewSalidomo(props: DetailedBatteryViewProps) {
|
|||
}}
|
||||
onClick={FirmwareModalHandleProceed}
|
||||
>
|
||||
Proceed
|
||||
<FormattedMessage id="proceed" defaultMessage="Proceed" />
|
||||
</Button>
|
||||
<Button
|
||||
sx={{
|
||||
|
|
@ -376,7 +376,7 @@ function DetailedBatteryViewSalidomo(props: DetailedBatteryViewProps) {
|
|||
}}
|
||||
onClick={FirmwareModalHandleCancel}
|
||||
>
|
||||
Cancel
|
||||
<FormattedMessage id="cancel" defaultMessage="Cancel" />
|
||||
</Button>
|
||||
</div>
|
||||
</Box>
|
||||
|
|
@ -410,8 +410,7 @@ function DetailedBatteryViewSalidomo(props: DetailedBatteryViewProps) {
|
|||
gutterBottom
|
||||
sx={{ fontWeight: 'bold' }}
|
||||
>
|
||||
The battery log is getting downloaded. It will be saved in the
|
||||
Downloads folder. Please wait...
|
||||
<FormattedMessage id="downloadingBatteryLog" defaultMessage="The battery log is getting downloaded. It will be saved in the Downloads folder. Please wait..." />
|
||||
</Typography>
|
||||
|
||||
<div
|
||||
|
|
@ -431,7 +430,7 @@ function DetailedBatteryViewSalidomo(props: DetailedBatteryViewProps) {
|
|||
}}
|
||||
onClick={StartDownloadBatteryLogModalHandleOk}
|
||||
>
|
||||
Ok
|
||||
<FormattedMessage id="ok" defaultMessage="Ok" />
|
||||
</Button>
|
||||
</div>
|
||||
</Box>
|
||||
|
|
@ -466,12 +465,11 @@ function DetailedBatteryViewSalidomo(props: DetailedBatteryViewProps) {
|
|||
gutterBottom
|
||||
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 variant="body1" gutterBottom>
|
||||
This action requires the battery service to be stopped for around
|
||||
10-15 minutes.
|
||||
<FormattedMessage id="batteryServiceStopWarning" defaultMessage="This action requires the battery service to be stopped for around 10-15 minutes." />
|
||||
</Typography>
|
||||
|
||||
<div
|
||||
|
|
@ -491,7 +489,7 @@ function DetailedBatteryViewSalidomo(props: DetailedBatteryViewProps) {
|
|||
}}
|
||||
onClick={DownloadBatteryLogModalHandleProceed}
|
||||
>
|
||||
Proceed
|
||||
<FormattedMessage id="proceed" defaultMessage="Proceed" />
|
||||
</Button>
|
||||
<Button
|
||||
sx={{
|
||||
|
|
@ -504,7 +502,7 @@ function DetailedBatteryViewSalidomo(props: DetailedBatteryViewProps) {
|
|||
}}
|
||||
onClick={DownloadBatteryLogModalHandleCancel}
|
||||
>
|
||||
Cancel
|
||||
<FormattedMessage id="cancel" defaultMessage="Cancel" />
|
||||
</Button>
|
||||
</div>
|
||||
</Box>
|
||||
|
|
@ -554,7 +552,7 @@ function DetailedBatteryViewSalidomo(props: DetailedBatteryViewProps) {
|
|||
}}
|
||||
onClick={ErrorModalHandleOk}
|
||||
>
|
||||
Ok
|
||||
<FormattedMessage id="ok" defaultMessage="Ok" />
|
||||
</Button>
|
||||
</div>
|
||||
</Box>
|
||||
|
|
|
|||
|
|
@ -494,7 +494,7 @@ function Log(props: LogProps) {
|
|||
{diag.description}
|
||||
</Typography>
|
||||
<Typography variant="caption" color="text.secondary">
|
||||
Last seen: {diag.lastSeen}
|
||||
<FormattedMessage id="lastSeen" defaultMessage="Last seen" />: {diag.lastSeen}
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ import PersonIcon from '@mui/icons-material/Person';
|
|||
import Button from '@mui/material/Button';
|
||||
import { Close as CloseIcon } from '@mui/icons-material';
|
||||
import { AccessContext } from 'src/contexts/AccessContextProvider';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
import { FormattedMessage, useIntl } from 'react-intl';
|
||||
import { UserType } from '../../../interfaces/UserTypes';
|
||||
|
||||
interface AccessProps {
|
||||
|
|
@ -35,6 +35,7 @@ interface AccessProps {
|
|||
}
|
||||
|
||||
function Access(props: AccessProps) {
|
||||
const intl = useIntl();
|
||||
const theme = useTheme();
|
||||
const tokencontext = useContext(TokenContext);
|
||||
const { removeToken } = tokencontext;
|
||||
|
|
@ -159,31 +160,11 @@ function Access(props: AccessProps) {
|
|||
|
||||
if (NotGrantedAccessUsers.length > 0) {
|
||||
setError(true);
|
||||
|
||||
const message =
|
||||
(
|
||||
<FormattedMessage
|
||||
id="unableToGrantAccess"
|
||||
defaultMessage="Unable to grant access to: "
|
||||
/>
|
||||
).props.defaultMessage +
|
||||
' ' +
|
||||
NotGrantedAccessUsers.join(', ');
|
||||
|
||||
setErrorMessage(message);
|
||||
setErrorMessage(intl.formatMessage({ id: 'unableToGrantAccess' }) + ' ' + NotGrantedAccessUsers.join(', '));
|
||||
}
|
||||
|
||||
if (grantedAccessUsers.length > 0) {
|
||||
const message =
|
||||
(
|
||||
<FormattedMessage
|
||||
id="grantedAccessToUsers"
|
||||
defaultMessage="Granted access to users: "
|
||||
/>
|
||||
).props.defaultMessage +
|
||||
' ' +
|
||||
grantedAccessUsers.join(', ');
|
||||
setUpdatedMessage(message);
|
||||
setUpdatedMessage(intl.formatMessage({ id: 'grantedAccessToUsers' }) + ' ' + grantedAccessUsers.join(', '));
|
||||
|
||||
setUpdated(true);
|
||||
|
||||
|
|
@ -306,7 +287,7 @@ function Access(props: AccessProps) {
|
|||
}}
|
||||
onClick={handleCloseFolder}
|
||||
>
|
||||
Ok
|
||||
<FormattedMessage id="ok" defaultMessage="Ok" />
|
||||
</Button>
|
||||
</Select>
|
||||
</FormControl>
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@ import PersonIcon from '@mui/icons-material/Person';
|
|||
import Button from '@mui/material/Button';
|
||||
import { Close as CloseIcon } from '@mui/icons-material';
|
||||
import { AccessContext } from 'src/contexts/AccessContextProvider';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
import { FormattedMessage, useIntl } from 'react-intl';
|
||||
import { InnovEnergyUser, UserType } from '../../../interfaces/UserTypes';
|
||||
import PersonRemoveIcon from '@mui/icons-material/PersonRemove';
|
||||
import {
|
||||
|
|
@ -47,6 +47,7 @@ function UserAccess(props: UserAccessProps) {
|
|||
return null;
|
||||
}
|
||||
|
||||
const intl = useIntl();
|
||||
const theme = useTheme();
|
||||
const tokencontext = useContext(TokenContext);
|
||||
const { removeToken } = tokencontext;
|
||||
|
|
@ -165,20 +166,12 @@ function UserAccess(props: UserAccessProps) {
|
|||
)
|
||||
.then((response) => {
|
||||
if (response) {
|
||||
setUpdatedMessage(
|
||||
'Granted access to user ' + props.current_user.name
|
||||
);
|
||||
setUpdatedMessage(intl.formatMessage({ id: 'grantedAccessToUser' }, { name: props.current_user.name }));
|
||||
setUpdated(true);
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
if (err.response && err.response.status === 401) {
|
||||
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');
|
||||
}
|
||||
setErrorMessage(intl.formatMessage({ id: 'errorOccured' }));
|
||||
setError(true);
|
||||
});
|
||||
}
|
||||
|
|
@ -194,20 +187,12 @@ function UserAccess(props: UserAccessProps) {
|
|||
)
|
||||
.then((response) => {
|
||||
if (response) {
|
||||
setUpdatedMessage(
|
||||
'Granted access to user ' + props.current_user.name
|
||||
);
|
||||
setUpdatedMessage(intl.formatMessage({ id: 'grantedAccessToUser' }, { name: props.current_user.name }));
|
||||
setUpdated(true);
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
if (err.response && err.response.status === 401) {
|
||||
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');
|
||||
}
|
||||
setErrorMessage(intl.formatMessage({ id: 'errorOccured' }));
|
||||
setError(true);
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -621,7 +621,7 @@ function Overview(props: OverviewProps) {
|
|||
</Container>
|
||||
)}
|
||||
|
||||
{!loading && (
|
||||
{!loading && dailyDataArray.length > 0 && (
|
||||
<Grid item xs={12} md={12}>
|
||||
{dailyData && (
|
||||
<Grid
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import { useEffect, useState } from 'react';
|
||||
import { useIntl, FormattedMessage } from 'react-intl';
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
|
|
@ -77,6 +78,7 @@ function FormattedBullet({ text }: { text: string }) {
|
|||
}
|
||||
|
||||
function WeeklyReport({ installationId }: WeeklyReportProps) {
|
||||
const intl = useIntl();
|
||||
const [report, setReport] = useState<WeeklyReportResponse | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
|
@ -89,14 +91,14 @@ function WeeklyReport({ installationId }: WeeklyReportProps) {
|
|||
|
||||
useEffect(() => {
|
||||
fetchReport();
|
||||
}, [installationId]);
|
||||
}, [installationId, intl.locale]);
|
||||
|
||||
const fetchReport = async () => {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
try {
|
||||
const res = await axiosConfig.get('/GetWeeklyReport', {
|
||||
params: { installationId }
|
||||
params: { installationId, language: intl.locale }
|
||||
});
|
||||
setReport(res.data);
|
||||
} catch (err: any) {
|
||||
|
|
@ -117,9 +119,9 @@ function WeeklyReport({ installationId }: WeeklyReportProps) {
|
|||
await axiosConfig.post('/SendWeeklyReportEmail', null, {
|
||||
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) {
|
||||
setSendStatus({ message: 'Failed to send. Please check the email address and try again.', severity: 'error' });
|
||||
setSendStatus({ message: intl.formatMessage({ id: 'reportSendError' }), severity: 'error' });
|
||||
} finally {
|
||||
setSending(false);
|
||||
}
|
||||
|
|
@ -138,7 +140,7 @@ function WeeklyReport({ installationId }: WeeklyReportProps) {
|
|||
>
|
||||
<CircularProgress size={50} style={{ color: '#ffc04d' }} />
|
||||
<Typography variant="body2" mt={2}>
|
||||
Generating weekly report...
|
||||
<FormattedMessage id="generatingReport" defaultMessage="Generating weekly report..." />
|
||||
</Typography>
|
||||
</Container>
|
||||
);
|
||||
|
|
@ -147,7 +149,7 @@ function WeeklyReport({ installationId }: WeeklyReportProps) {
|
|||
if (error) {
|
||||
return (
|
||||
<Container sx={{ py: 4 }}>
|
||||
<Alert severity="warning">{error}</Alert>
|
||||
<Alert severity="warning"><FormattedMessage id="noReportData" defaultMessage="No report data found." /></Alert>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
|
@ -201,7 +203,7 @@ function WeeklyReport({ installationId }: WeeklyReportProps) {
|
|||
disabled={sending || !email.trim()}
|
||||
sx={{ bgcolor: '#2c3e50', '&:hover': { bgcolor: '#34495e' } }}
|
||||
>
|
||||
Send Report
|
||||
<FormattedMessage id="sendReport" defaultMessage="Send Report" />
|
||||
</Button>
|
||||
</Box>
|
||||
{sendStatus && (
|
||||
|
|
@ -222,7 +224,7 @@ function WeeklyReport({ installationId }: WeeklyReportProps) {
|
|||
}}
|
||||
>
|
||||
<Typography variant="h5" fontWeight="bold">
|
||||
Weekly Performance Report
|
||||
<FormattedMessage id="reportTitle" defaultMessage="Weekly Performance Report" />
|
||||
</Typography>
|
||||
<Typography variant="body1" sx={{ opacity: 0.9, mt: 0.5 }}>
|
||||
{report.installationName}
|
||||
|
|
@ -235,7 +237,7 @@ function WeeklyReport({ installationId }: WeeklyReportProps) {
|
|||
{/* Weekly Insights (was AI Insights) */}
|
||||
<Paper sx={{ p: 3, mb: 3 }}>
|
||||
<Typography variant="h6" fontWeight="bold" sx={{ mb: 2, color: '#2c3e50' }}>
|
||||
Weekly Insights
|
||||
<FormattedMessage id="weeklyInsights" defaultMessage="Weekly Insights" />
|
||||
</Typography>
|
||||
<Box
|
||||
sx={{
|
||||
|
|
@ -262,38 +264,38 @@ function WeeklyReport({ installationId }: WeeklyReportProps) {
|
|||
{/* Your Savings This Week */}
|
||||
<Paper sx={{ p: 3, mb: 3 }}>
|
||||
<Typography variant="h6" fontWeight="bold" sx={{ mb: 2, color: '#2c3e50' }}>
|
||||
Your Savings This Week
|
||||
<FormattedMessage id="weeklySavings" defaultMessage="Your Savings This Week" />
|
||||
</Typography>
|
||||
<Grid container spacing={2}>
|
||||
<Grid item xs={6} sm={3}>
|
||||
<SavingsCard
|
||||
label="Solar Energy Used"
|
||||
label={intl.formatMessage({ id: 'solarEnergyUsed' })}
|
||||
value={`${solarSavingsKwh} kWh`}
|
||||
subtitle="of your solar stayed at home"
|
||||
subtitle={intl.formatMessage({ id: 'solarStayedHome' })}
|
||||
color="#27ae60"
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item xs={6} sm={3}>
|
||||
<SavingsCard
|
||||
label="Est. Money Saved"
|
||||
label={intl.formatMessage({ id: 'estMoneySaved' })}
|
||||
value={`~${estimatedSavingsCHF} CHF`}
|
||||
subtitle="at 0.27 CHF/kWh avg."
|
||||
subtitle={intl.formatMessage({ id: 'atCHFRate' })}
|
||||
color="#2980b9"
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item xs={6} sm={3}>
|
||||
<SavingsCard
|
||||
label="Solar Coverage"
|
||||
label={intl.formatMessage({ id: 'solarCoverage' })}
|
||||
value={`${report.selfSufficiencyPercent.toFixed(0)}%`}
|
||||
subtitle="of consumption from solar"
|
||||
subtitle={intl.formatMessage({ id: 'fromSolarSub' })}
|
||||
color="#8e44ad"
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item xs={6} sm={3}>
|
||||
<SavingsCard
|
||||
label="Battery Efficiency"
|
||||
label={intl.formatMessage({ id: 'batteryEfficiency' })}
|
||||
value={`${report.batteryEfficiencyPercent.toFixed(0)}%`}
|
||||
subtitle="energy out vs energy in"
|
||||
subtitle={intl.formatMessage({ id: 'batteryEffSub' })}
|
||||
color="#e67e22"
|
||||
/>
|
||||
</Grid>
|
||||
|
|
@ -303,44 +305,44 @@ function WeeklyReport({ installationId }: WeeklyReportProps) {
|
|||
{/* Weekly Summary Table */}
|
||||
<Paper sx={{ p: 3, mb: 3 }}>
|
||||
<Typography variant="h6" fontWeight="bold" sx={{ mb: 2, color: '#2c3e50' }}>
|
||||
Weekly Summary
|
||||
<FormattedMessage id="weeklySummary" defaultMessage="Weekly Summary" />
|
||||
</Typography>
|
||||
<Box component="table" sx={{ width: '100%', borderCollapse: 'collapse', '& td, & th': { p: 1.2, borderBottom: '1px solid #eee' } }}>
|
||||
<thead>
|
||||
<tr style={{ background: '#f8f9fa' }}>
|
||||
<th style={{ textAlign: 'left' }}>Metric</th>
|
||||
<th style={{ textAlign: 'right' }}>This Week</th>
|
||||
{prev && <th style={{ textAlign: 'right' }}>Last Week</th>}
|
||||
{prev && <th style={{ textAlign: 'right' }}>Change</th>}
|
||||
<th style={{ textAlign: 'left' }}><FormattedMessage id="metric" defaultMessage="Metric" /></th>
|
||||
<th style={{ textAlign: 'right' }}><FormattedMessage id="thisWeek" defaultMessage="This Week" /></th>
|
||||
{prev && <th style={{ textAlign: 'right' }}><FormattedMessage id="lastWeek" defaultMessage="Last Week" /></th>}
|
||||
{prev && <th style={{ textAlign: 'right' }}><FormattedMessage id="change" defaultMessage="Change" /></th>}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<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>
|
||||
{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>}
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Consumption</td>
|
||||
<td><FormattedMessage id="consumption" defaultMessage="Consumption" /></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: changeColor(report.consumptionChangePercent, true) }}>{formatChange(report.consumptionChangePercent)}</td>}
|
||||
</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>
|
||||
{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>}
|
||||
</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>
|
||||
{prev && <td style={{ textAlign: 'right', color: '#888' }}>{prev.totalGridExport.toFixed(1)} kWh</td>}
|
||||
{prev && <td style={{ textAlign: 'right' }}>—</td>}
|
||||
</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>
|
||||
{prev && <td style={{ textAlign: 'right', color: '#888' }}>{prev.totalBatteryCharged.toFixed(1)} / {prev.totalBatteryDischarged.toFixed(1)} kWh</td>}
|
||||
{prev && <td style={{ textAlign: 'right' }}>—</td>}
|
||||
|
|
@ -353,18 +355,18 @@ function WeeklyReport({ installationId }: WeeklyReportProps) {
|
|||
{report.dailyData.length > 0 && (
|
||||
<Paper sx={{ p: 3, mb: 3 }}>
|
||||
<Typography variant="h6" fontWeight="bold" sx={{ mb: 1, color: '#2c3e50' }}>
|
||||
Daily Breakdown
|
||||
<FormattedMessage id="dailyBreakdown" defaultMessage="Daily Breakdown" />
|
||||
</Typography>
|
||||
{/* Legend */}
|
||||
<Box sx={{ display: 'flex', gap: 3, mb: 2, fontSize: '12px', color: '#666' }}>
|
||||
<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 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 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>
|
||||
{/* Bars */}
|
||||
|
|
@ -377,7 +379,7 @@ function WeeklyReport({ installationId }: WeeklyReportProps) {
|
|||
<Box sx={{ display: 'flex', justifyContent: 'space-between', mb: 0.3 }}>
|
||||
<Typography variant="caption" sx={{ fontWeight: isCurrentWeek ? 'bold' : 'normal', color: '#444', fontSize: '12px' }}>
|
||||
{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 variant="caption" sx={{ color: '#888', fontSize: '11px' }}>
|
||||
PV {d.pvProduction.toFixed(1)} | Load {d.loadConsumption.toFixed(1)} | Grid {d.gridImport.toFixed(1)} kWh
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ import {
|
|||
I_UserWithInheritedAccess,
|
||||
InnovEnergyUser
|
||||
} from '../interfaces/UserTypes';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
import { useIntl } from 'react-intl';
|
||||
import { I_Installation } from '../interfaces/InstallationTypes';
|
||||
|
||||
interface AccessContextProviderProps {
|
||||
|
|
@ -69,10 +69,11 @@ export const AccessContext = createContext<AccessContextProviderProps>({
|
|||
});
|
||||
|
||||
const AccessContextProvider = ({ children }: { children: ReactNode }) => {
|
||||
const intl = useIntl();
|
||||
const [error, setError] = useState(false);
|
||||
const [errormessage, setErrorMessage] = useState('An error has occured');
|
||||
const [errormessage, setErrorMessage] = useState('');
|
||||
const [updated, setUpdated] = useState(false);
|
||||
const [updatedmessage, setUpdatedMessage] = useState('Successfully updated');
|
||||
const [updatedmessage, setUpdatedMessage] = useState('');
|
||||
const tokencontext = useContext(TokenContext);
|
||||
const { removeToken } = tokencontext;
|
||||
const [usersWithDirectAccess, setUsersWithDirectAccess] = useState<
|
||||
|
|
@ -95,20 +96,12 @@ const AccessContextProvider = ({ children }: { children: ReactNode }) => {
|
|||
setUsersWithDirectAccess(response.data);
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
.catch(() => {
|
||||
setError(true);
|
||||
|
||||
const message = (
|
||||
<FormattedMessage
|
||||
id="unableToLoadData"
|
||||
defaultMessage="Unable to load data"
|
||||
/>
|
||||
).props.defaultMessage;
|
||||
|
||||
setErrorMessage(message);
|
||||
setErrorMessage(intl.formatMessage({ id: 'unableToLoadData' }));
|
||||
});
|
||||
},
|
||||
[]
|
||||
[intl]
|
||||
);
|
||||
|
||||
const fetchInstallationsForUser = useCallback(async (userId: number) => {
|
||||
|
|
@ -119,17 +112,11 @@ const AccessContextProvider = ({ children }: { children: ReactNode }) => {
|
|||
setAccessibleInstallationsForUser(response.data);
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
.catch(() => {
|
||||
setError(true);
|
||||
const message = (
|
||||
<FormattedMessage
|
||||
id="unableToLoadData"
|
||||
defaultMessage="Unable to load data"
|
||||
/>
|
||||
).props.defaultMessage;
|
||||
setErrorMessage(message);
|
||||
setErrorMessage(intl.formatMessage({ id: 'unableToLoadData' }));
|
||||
});
|
||||
}, []);
|
||||
}, [intl]);
|
||||
|
||||
const fetchUsersWithInheritedAccessForResource = useCallback(
|
||||
async (tempresourceType: string, id: number) => {
|
||||
|
|
@ -140,18 +127,12 @@ const AccessContextProvider = ({ children }: { children: ReactNode }) => {
|
|||
setUsersWithInheritedAccess(response.data);
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
.catch(() => {
|
||||
setError(true);
|
||||
const message = (
|
||||
<FormattedMessage
|
||||
id="unableToLoadData"
|
||||
defaultMessage="Unable to load data"
|
||||
/>
|
||||
).props.defaultMessage;
|
||||
setErrorMessage(message);
|
||||
setErrorMessage(intl.formatMessage({ id: 'unableToLoadData' }));
|
||||
});
|
||||
},
|
||||
[]
|
||||
[intl]
|
||||
);
|
||||
|
||||
const fetchAvailableUsers = async (): Promise<void> => {
|
||||
|
|
@ -183,17 +164,7 @@ const AccessContextProvider = ({ children }: { children: ReactNode }) => {
|
|||
current_ResourceId
|
||||
);
|
||||
|
||||
const message =
|
||||
(
|
||||
<FormattedMessage
|
||||
id="revokedAccessFromUser"
|
||||
defaultMessage="Revoked access from user: "
|
||||
/>
|
||||
).props.defaultMessage +
|
||||
' ' +
|
||||
name;
|
||||
|
||||
setUpdatedMessage(message);
|
||||
setUpdatedMessage(intl.formatMessage({ id: 'revokedAccessFromUser' }) + ' ' + name);
|
||||
|
||||
setUpdated(true);
|
||||
setTimeout(() => {
|
||||
|
|
@ -201,19 +172,12 @@ const AccessContextProvider = ({ children }: { children: ReactNode }) => {
|
|||
}, 3000);
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
.catch(() => {
|
||||
setError(true);
|
||||
const message = (
|
||||
<FormattedMessage
|
||||
id="unableToRevokeAccess"
|
||||
defaultMessage="Unable to revoke access"
|
||||
/>
|
||||
).props.defaultMessage;
|
||||
|
||||
setErrorMessage(message);
|
||||
setErrorMessage(intl.formatMessage({ id: 'unableToRevokeAccess' }));
|
||||
});
|
||||
},
|
||||
[]
|
||||
[intl, fetchUsersWithDirectAccessForResource, fetchUsersWithInheritedAccessForResource]
|
||||
);
|
||||
|
||||
return (
|
||||
|
|
|
|||
|
|
@ -13,7 +13,9 @@
|
|||
"english": "Englisch",
|
||||
"error": "Fehler",
|
||||
"folder": "Ordner",
|
||||
"french": "Französisch",
|
||||
"german": "Deutsch",
|
||||
"italian": "Italienisch",
|
||||
"groupTabs": "Gruppen",
|
||||
"groupTree": "Gruppenbaum",
|
||||
"overview": "Überblick",
|
||||
|
|
@ -89,5 +91,44 @@
|
|||
"unableToGrantAccess": "Der Zugriff kann nicht gewährt werden",
|
||||
"unableToLoadData": "Daten können nicht geladen 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",
|
||||
"english": "English",
|
||||
"german": "German",
|
||||
"french": "French",
|
||||
"italian": "Italian",
|
||||
"language": "Language",
|
||||
"installation": "Installation",
|
||||
"location": "Location",
|
||||
"log": "Log",
|
||||
|
|
@ -70,5 +73,44 @@
|
|||
"unableToGrantAccess": "Unable to grant access to: ",
|
||||
"unableToLoadData": "Unable to load data",
|
||||
"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",
|
||||
"error": "Erreur",
|
||||
"folder": "Dossier",
|
||||
"french": "Français",
|
||||
"german": "Allemand",
|
||||
"italian": "Italien",
|
||||
"language": "Langue",
|
||||
"overview": "Aperçu",
|
||||
"manage": "Gestion des accès",
|
||||
"configuration": "Configuration",
|
||||
|
|
@ -19,7 +22,6 @@
|
|||
"apply_changes": "Appliquer",
|
||||
"delete_user": "Supprimer l'utilisateur",
|
||||
"installation_name_simple": "Nom de l'installation: ",
|
||||
"language": "Langue",
|
||||
"minimum_soc": "Soc minimum",
|
||||
"calibration_charge_forced": "Charge d'étalonnage forcée",
|
||||
"grid_set_point": "Point de consigne de grid",
|
||||
|
|
@ -41,7 +43,7 @@
|
|||
"lastWeek": "La semaine dernière",
|
||||
"location": "Localité",
|
||||
"log": "Journal",
|
||||
"logout": "Fermer las session",
|
||||
"logout": "Fermer la session",
|
||||
"makeASelection": "Veuillez faire une sélection à gauche",
|
||||
"manageAccess": "Gérer l'accès",
|
||||
"move": "Déplacer",
|
||||
|
|
@ -63,7 +65,7 @@
|
|||
"status": "Statut",
|
||||
"live": "Diffusion en direct",
|
||||
"deleteInstallation": "Supprimer l'installation",
|
||||
"errorOccured": "Une erreur s’est produite",
|
||||
"errorOccured": "Une erreur s'est produite",
|
||||
"successfullyUpdated": "Mise à jour réussie",
|
||||
"grantAccess": "Accorder l'accès",
|
||||
"UserswithDirectAccess": "Utilisateurs avec accès direct",
|
||||
|
|
@ -83,5 +85,44 @@
|
|||
"unableToGrantAccess": "Impossible d'accorder l'accès à",
|
||||
"unableToLoadData": "Impossible de charger les données",
|
||||
"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
|
||||
</MenuItem>
|
||||
<MenuItem value="de" onClick={() => handleLanguageSelect('de')}>
|
||||
German
|
||||
Deutsch
|
||||
</MenuItem>
|
||||
<MenuItem value="fr" onClick={() => handleLanguageSelect('fr')}>
|
||||
French
|
||||
Français
|
||||
</MenuItem>
|
||||
<MenuItem value="it" onClick={() => handleLanguageSelect('it')}>
|
||||
Italiano
|
||||
</MenuItem>
|
||||
</Menu>
|
||||
</div>
|
||||
|
|
|
|||
Loading…
Reference in New Issue