Compare commits
10 Commits
41119565ae
...
1e7c500f90
| Author | SHA1 | Date |
|---|---|---|
|
|
1e7c500f90 | |
|
|
c94eb235a9 | |
|
|
706e0674fb | |
|
|
dc5b09d1f2 | |
|
|
be264c2165 | |
|
|
b0bcf06d4e | |
|
|
e989db54c2 | |
|
|
c4b293fb74 | |
|
|
4bc6712e60 | |
|
|
015cd5e5e6 |
|
|
@ -922,8 +922,85 @@ public class Controller : ControllerBase
|
|||
});
|
||||
}
|
||||
|
||||
// ── Email Preferences ──────────────────────────────────────────────
|
||||
|
||||
[HttpGet(nameof(GetEmailPreference))]
|
||||
public ActionResult GetEmailPreference(Int64 installationId, Token authToken)
|
||||
{
|
||||
var user = Db.GetSession(authToken)?.User;
|
||||
if (user == null) return Unauthorized();
|
||||
|
||||
var pref = Db.GetEmailPreference(installationId);
|
||||
return Ok(new
|
||||
{
|
||||
installationId,
|
||||
sendWeekly = pref?.SendWeekly ?? false,
|
||||
sendMonthly = pref?.SendMonthly ?? false,
|
||||
sendYearly = pref?.SendYearly ?? false
|
||||
});
|
||||
}
|
||||
|
||||
[HttpPost(nameof(UpdateEmailPreference))]
|
||||
public ActionResult UpdateEmailPreference(
|
||||
Int64 installationId, Boolean sendWeekly, Boolean sendMonthly,
|
||||
Boolean sendYearly, Token authToken)
|
||||
{
|
||||
var user = Db.GetSession(authToken)?.User;
|
||||
if (user == null) return Unauthorized();
|
||||
|
||||
Db.UpsertEmailPreference(new EmailPreference
|
||||
{
|
||||
InstallationId = installationId,
|
||||
SendWeekly = sendWeekly,
|
||||
SendMonthly = sendMonthly,
|
||||
SendYearly = sendYearly
|
||||
});
|
||||
|
||||
return Ok();
|
||||
}
|
||||
|
||||
// ── Weekly Performance Report ──────────────────────────────────────
|
||||
|
||||
private async Task<WeeklyReportResponse?> FetchWeeklyReportAsync(
|
||||
Int64 installationId, String installationName, String lang,
|
||||
DateOnly? weekStartDate = null, Boolean forceRegenerate = false)
|
||||
{
|
||||
DateOnly periodStart, periodEnd;
|
||||
if (weekStartDate.HasValue)
|
||||
{
|
||||
periodStart = weekStartDate.Value;
|
||||
periodEnd = weekStartDate.Value.AddDays(6);
|
||||
}
|
||||
else
|
||||
{
|
||||
(periodStart, periodEnd) = WeeklyReportService.LastCalendarWeek();
|
||||
}
|
||||
|
||||
var periodStartStr = periodStart.ToString("yyyy-MM-dd");
|
||||
var periodEndStr = periodEnd.ToString("yyyy-MM-dd");
|
||||
|
||||
if (!forceRegenerate)
|
||||
{
|
||||
var cached = Db.GetWeeklyReportForWeek(installationId, periodStartStr, periodEndStr);
|
||||
if (cached != null)
|
||||
{
|
||||
var cachedResponse = await ReportAggregationService.ToWeeklyReportResponseAsync(cached, lang);
|
||||
if (cachedResponse != null)
|
||||
{
|
||||
Console.WriteLine($"[WeeklyReport] Serving cached report for installation {installationId}, period {periodStartStr}–{periodEndStr}, language={lang}");
|
||||
return cachedResponse;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Console.WriteLine($"[WeeklyReport] Generating fresh report for installation {installationId}, period {periodStartStr}–{periodEndStr}");
|
||||
var report = await WeeklyReportService.GenerateReportAsync(
|
||||
installationId, installationName, lang, weekStartDate);
|
||||
|
||||
ReportAggregationService.SaveWeeklySummary(installationId, report, lang);
|
||||
return report;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns a weekly performance report. Serves from cache if available;
|
||||
/// generates fresh on first request or when forceRegenerate is true.
|
||||
|
|
@ -954,43 +1031,9 @@ public class Controller : ControllerBase
|
|||
{
|
||||
var lang = language ?? user.Language ?? "en";
|
||||
|
||||
// Compute target week dates for cache lookup
|
||||
DateOnly periodStart, periodEnd;
|
||||
if (weekStartDate.HasValue)
|
||||
{
|
||||
periodStart = weekStartDate.Value;
|
||||
periodEnd = weekStartDate.Value.AddDays(6);
|
||||
}
|
||||
else
|
||||
{
|
||||
(periodStart, periodEnd) = WeeklyReportService.LastCalendarWeek();
|
||||
}
|
||||
|
||||
var periodStartStr = periodStart.ToString("yyyy-MM-dd");
|
||||
var periodEndStr = periodEnd.ToString("yyyy-MM-dd");
|
||||
|
||||
// Cache-first: check if a cached report exists for this week
|
||||
if (!forceRegenerate)
|
||||
{
|
||||
var cached = Db.GetWeeklyReportForWeek(installationId, periodStartStr, periodEndStr);
|
||||
if (cached != null)
|
||||
{
|
||||
var cachedResponse = await ReportAggregationService.ToWeeklyReportResponseAsync(cached, lang);
|
||||
if (cachedResponse != null)
|
||||
{
|
||||
Console.WriteLine($"[GetWeeklyReport] Serving cached report for installation {installationId}, period {periodStartStr}–{periodEndStr}, language={lang}");
|
||||
return Ok(cachedResponse);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Cache miss or forceRegenerate: generate fresh
|
||||
Console.WriteLine($"[GetWeeklyReport] Generating fresh report for installation {installationId}, period {periodStartStr}–{periodEndStr}");
|
||||
var report = await WeeklyReportService.GenerateReportAsync(
|
||||
installationId, installation.Name, lang, weekStartDate);
|
||||
|
||||
// Persist weekly summary and seed AiInsightCache for this language
|
||||
ReportAggregationService.SaveWeeklySummary(installationId, report, lang);
|
||||
var report = await FetchWeeklyReportAsync(installationId, installation.Name, lang, weekStartDate, forceRegenerate);
|
||||
if (report == null)
|
||||
return BadRequest("Failed to generate report.");
|
||||
|
||||
return Ok(report);
|
||||
}
|
||||
|
|
@ -1097,6 +1140,46 @@ public class Controller : ControllerBase
|
|||
return Ok(reports);
|
||||
}
|
||||
|
||||
[HttpGet(nameof(GetCurrentMonthPreview))]
|
||||
public async Task<ActionResult<MonthlyReportSummary>> GetCurrentMonthPreview(
|
||||
Int64 installationId, Token authToken, String? language = null)
|
||||
{
|
||||
var user = Db.GetSession(authToken)?.User;
|
||||
if (user == null)
|
||||
return Unauthorized();
|
||||
|
||||
var installation = Db.GetInstallationById(installationId);
|
||||
if (installation is null || !user.HasAccessTo(installation))
|
||||
return Unauthorized();
|
||||
|
||||
var lang = language ?? user.Language ?? "en";
|
||||
var preview = await ReportAggregationService.GetCurrentMonthPreviewAsync(installationId, lang);
|
||||
if (preview == null)
|
||||
return NotFound("No daily data for the current month.");
|
||||
|
||||
return Ok(preview);
|
||||
}
|
||||
|
||||
[HttpGet(nameof(GetCurrentYearPreview))]
|
||||
public async Task<ActionResult<YearlyReportSummary>> GetCurrentYearPreview(
|
||||
Int64 installationId, Token authToken, String? language = null)
|
||||
{
|
||||
var user = Db.GetSession(authToken)?.User;
|
||||
if (user == null)
|
||||
return Unauthorized();
|
||||
|
||||
var installation = Db.GetInstallationById(installationId);
|
||||
if (installation is null || !user.HasAccessTo(installation))
|
||||
return Unauthorized();
|
||||
|
||||
var lang = language ?? user.Language ?? "en";
|
||||
var preview = await ReportAggregationService.GetCurrentYearPreviewAsync(installationId, lang);
|
||||
if (preview == null)
|
||||
return NotFound("No monthly reports for the current year.");
|
||||
|
||||
return Ok(preview);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Manually trigger monthly aggregation for an installation.
|
||||
/// Computes monthly report from daily records for the specified year/month.
|
||||
|
|
@ -1477,7 +1560,9 @@ public class Controller : ControllerBase
|
|||
// ── Report HTML (for PDF download) ─────────────────────────────
|
||||
|
||||
[HttpGet(nameof(GetWeeklyReportHtml))]
|
||||
public async Task<ActionResult> GetWeeklyReportHtml(Int64 installationId, Token authToken, String? language = null)
|
||||
public async Task<ActionResult> GetWeeklyReportHtml(
|
||||
Int64 installationId, Token authToken,
|
||||
String? language = null, String? weekStart = null, String source = "email")
|
||||
{
|
||||
var user = Db.GetSession(authToken)?.User;
|
||||
if (user == null) return Unauthorized();
|
||||
|
|
@ -1485,14 +1570,26 @@ public class Controller : ControllerBase
|
|||
var installation = Db.GetInstallationById(installationId);
|
||||
if (installation is null || !user.HasAccessTo(installation)) return Unauthorized();
|
||||
|
||||
var lang = language ?? user.Language ?? "en";
|
||||
var report = await WeeklyReportService.GenerateReportAsync(installationId, installation.Name, lang);
|
||||
var html = ReportEmailService.BuildHtmlEmail(report, lang);
|
||||
var lang = language ?? user.Language ?? "en";
|
||||
|
||||
DateOnly? weekStartDate = null;
|
||||
if (!String.IsNullOrEmpty(weekStart))
|
||||
{
|
||||
if (!DateOnly.TryParseExact(weekStart, "yyyy-MM-dd", out var parsed))
|
||||
return BadRequest("weekStart must be in yyyy-MM-dd format.");
|
||||
weekStartDate = parsed;
|
||||
}
|
||||
|
||||
var report = await FetchWeeklyReportAsync(installationId, installation.Name, lang, weekStartDate);
|
||||
if (report == null)
|
||||
return BadRequest("Failed to generate report.");
|
||||
|
||||
var html = ReportEmailService.BuildHtmlEmail(report, lang, source: source);
|
||||
return Content(html, "text/html");
|
||||
}
|
||||
|
||||
[HttpGet(nameof(GetMonthlyReportHtml))]
|
||||
public async Task<ActionResult> GetMonthlyReportHtml(Int64 installationId, Int32 year, Int32 month, Token authToken, String? language = null)
|
||||
public async Task<ActionResult> GetMonthlyReportHtml(Int64 installationId, Int32 year, Int32 month, Token authToken, String? language = null, String source = "email")
|
||||
{
|
||||
var user = Db.GetSession(authToken)?.User;
|
||||
if (user == null) return Unauthorized();
|
||||
|
|
@ -1511,12 +1608,12 @@ public class Controller : ControllerBase
|
|||
report.TotalPvProduction, report.TotalConsumption, report.TotalGridImport, report.TotalGridExport,
|
||||
report.TotalBatteryCharged, report.TotalBatteryDischarged, report.TotalEnergySaved, report.TotalSavingsCHF,
|
||||
report.SelfSufficiencyPercent, report.BatteryEfficiencyPercent, report.AiInsight,
|
||||
$"{report.WeekCount} {s.CountLabel}", s);
|
||||
$"{report.WeekCount} {s.CountLabel}", s, source: source);
|
||||
return Content(html, "text/html");
|
||||
}
|
||||
|
||||
[HttpGet(nameof(GetYearlyReportHtml))]
|
||||
public async Task<ActionResult> GetYearlyReportHtml(Int64 installationId, Int32 year, Token authToken, String? language = null)
|
||||
public async Task<ActionResult> GetYearlyReportHtml(Int64 installationId, Int32 year, Token authToken, String? language = null, String source = "email")
|
||||
{
|
||||
var user = Db.GetSession(authToken)?.User;
|
||||
if (user == null) return Unauthorized();
|
||||
|
|
@ -1535,7 +1632,7 @@ public class Controller : ControllerBase
|
|||
report.TotalPvProduction, report.TotalConsumption, report.TotalGridImport, report.TotalGridExport,
|
||||
report.TotalBatteryCharged, report.TotalBatteryDischarged, report.TotalEnergySaved, report.TotalSavingsCHF,
|
||||
report.SelfSufficiencyPercent, report.BatteryEfficiencyPercent, report.AiInsight,
|
||||
$"{report.MonthCount} {s.CountLabel}", s);
|
||||
$"{report.MonthCount} {s.CountLabel}", s, source: source);
|
||||
return Content(html, "text/html");
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,12 @@
|
|||
using SQLite;
|
||||
|
||||
namespace InnovEnergy.App.Backend.DataTypes;
|
||||
|
||||
public class EmailPreference
|
||||
{
|
||||
[PrimaryKey]
|
||||
public Int64 InstallationId { get; set; }
|
||||
public Boolean SendWeekly { get; set; }
|
||||
public Boolean SendMonthly { get; set; }
|
||||
public Boolean SendYearly { get; set; }
|
||||
}
|
||||
|
|
@ -67,4 +67,5 @@ public class Installation : TreeNode
|
|||
public String VrmLink { get; set; } = "";
|
||||
public string Configuration { get; set; } = "";
|
||||
public string NetworkProvider { get; set; } = "";
|
||||
public string Email { get; set; } = "";
|
||||
}
|
||||
|
|
@ -94,6 +94,11 @@ public class MonthlyReportSummary
|
|||
public Int32 WeekCount { get; set; }
|
||||
public String AiInsight { get; set; } = "";
|
||||
public String CreatedAt { get; set; } = "";
|
||||
|
||||
// Preview-only fields (not stored in DB)
|
||||
[Ignore] public Boolean IsPreview { get; set; }
|
||||
[Ignore] public Int32 DaysAvailable { get; set; }
|
||||
[Ignore] public Int32 DaysInMonth { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -137,19 +142,22 @@ public class YearlyReportSummary
|
|||
public Int32 MonthCount { get; set; }
|
||||
public String AiInsight { get; set; } = "";
|
||||
public String CreatedAt { get; set; } = "";
|
||||
|
||||
// Preview-only fields (not stored in DB)
|
||||
[Ignore] public Boolean IsPreview { get; set; }
|
||||
}
|
||||
|
||||
// ── DTOs for pending aggregation queries (not stored in DB) ──
|
||||
|
||||
public class PendingMonth
|
||||
{
|
||||
public Int32 Year { get; set; }
|
||||
public Int32 Month { get; set; }
|
||||
public Int32 WeekCount { get; set; }
|
||||
public Int32 Year { get; set; }
|
||||
public Int32 Month { get; set; }
|
||||
public Int32 WeekCount { get; set; }
|
||||
}
|
||||
|
||||
public class PendingYear
|
||||
{
|
||||
public Int32 Year { get; set; }
|
||||
public Int32 MonthCount { get; set; }
|
||||
public Int32 Year { get; set; }
|
||||
public Int32 MonthCount { get; set; }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -75,6 +75,13 @@ public static partial class Db
|
|||
public static Boolean Create(HourlyEnergyRecord record) => Insert(record);
|
||||
public static Boolean Create(AiInsightCache cache) => Insert(cache);
|
||||
|
||||
public static Boolean UpsertEmailPreference(EmailPreference pref)
|
||||
{
|
||||
var success = Connection.InsertOrReplace(pref) > 0;
|
||||
if (success) Backup();
|
||||
return success;
|
||||
}
|
||||
|
||||
// Ticket system
|
||||
public static Boolean Create(Ticket ticket) => Insert(ticket);
|
||||
public static Boolean Create(TicketComment comment) => Insert(comment);
|
||||
|
|
|
|||
|
|
@ -31,6 +31,7 @@ public static partial class Db
|
|||
public static TableQuery<DailyEnergyRecord> DailyRecords => Connection.Table<DailyEnergyRecord>();
|
||||
public static TableQuery<HourlyEnergyRecord> HourlyRecords => Connection.Table<HourlyEnergyRecord>();
|
||||
public static TableQuery<AiInsightCache> AiInsightCaches => Connection.Table<AiInsightCache>();
|
||||
public static TableQuery<EmailPreference> EmailPreferences => Connection.Table<EmailPreference>();
|
||||
|
||||
// Ticket system tables
|
||||
public static TableQuery<Ticket> Tickets => Connection.Table<Ticket>();
|
||||
|
|
@ -69,6 +70,7 @@ public static partial class Db
|
|||
Connection.CreateTable<DailyEnergyRecord>();
|
||||
Connection.CreateTable<HourlyEnergyRecord>();
|
||||
Connection.CreateTable<AiInsightCache>();
|
||||
Connection.CreateTable<EmailPreference>();
|
||||
|
||||
// Ticket system tables
|
||||
Connection.CreateTable<Ticket>();
|
||||
|
|
@ -125,6 +127,7 @@ public static partial class Db
|
|||
fileConnection.CreateTable<DailyEnergyRecord>();
|
||||
fileConnection.CreateTable<HourlyEnergyRecord>();
|
||||
fileConnection.CreateTable<AiInsightCache>();
|
||||
fileConnection.CreateTable<EmailPreference>();
|
||||
|
||||
// Ticket system tables
|
||||
fileConnection.CreateTable<Ticket>();
|
||||
|
|
|
|||
|
|
@ -161,6 +161,11 @@ public static partial class Db
|
|||
&& c.Language == language)
|
||||
?.InsightText;
|
||||
|
||||
// ── EmailPreference Queries ─────────────────────────────────────────
|
||||
|
||||
public static EmailPreference? GetEmailPreference(Int64 installationId)
|
||||
=> EmailPreferences.FirstOrDefault(p => p.InstallationId == installationId);
|
||||
|
||||
// ── Ticket Queries ──────────────────────────────────────────────────
|
||||
|
||||
public static Ticket? GetTicketById(Int64 id)
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using InnovEnergy.App.Backend.DataTypes;
|
||||
|
|
@ -115,6 +116,7 @@ public static class AggregatedJsonParser
|
|||
/// Tries to read an aggregated JSON file from the installation's S3 bucket.
|
||||
/// S3 key: DDMMYYYY.json (directly in bucket root).
|
||||
/// Returns file content or null if not found / error.
|
||||
/// Handles base64-encoded files (SinexcelCommunication uploads base64).
|
||||
/// </summary>
|
||||
public static async Task<String?> TryReadFromS3(Installation installation, String isoDate)
|
||||
{
|
||||
|
|
@ -125,7 +127,8 @@ public static class AggregatedJsonParser
|
|||
var bucket = region.Bucket(installation.BucketName());
|
||||
var s3Url = bucket.Path(fileName);
|
||||
|
||||
return await s3Url.GetObjectAsString();
|
||||
var raw = await s3Url.GetObjectAsString();
|
||||
return DecodeContent(raw);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
|
|
@ -134,6 +137,29 @@ public static class AggregatedJsonParser
|
|||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Decodes S3 file content. SinexcelCommunication devices upload DDMMYYYY.json
|
||||
/// as base64-encoded NDJSON. If the content doesn't start with '{' it is
|
||||
/// assumed to be base64 and decoded accordingly.
|
||||
/// </summary>
|
||||
private static String DecodeContent(String raw)
|
||||
{
|
||||
var trimmed = raw.Trim();
|
||||
if (trimmed.StartsWith('{'))
|
||||
return raw;
|
||||
|
||||
try
|
||||
{
|
||||
var decoded = Encoding.UTF8.GetString(Convert.FromBase64String(trimmed));
|
||||
Console.WriteLine("[AggregatedJsonParser] Decoded base64-encoded S3 content");
|
||||
return decoded;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return raw;
|
||||
}
|
||||
}
|
||||
|
||||
// --- JSON DTOs ---
|
||||
|
||||
private sealed class HourlyJsonDto
|
||||
|
|
|
|||
|
|
@ -121,6 +121,9 @@ public static class ReportAggregationService
|
|||
generated++;
|
||||
|
||||
Console.WriteLine($"[ReportAggregation] Weekly report generated for installation {installation.Id} ({installation.Name})");
|
||||
|
||||
// Auto-send email if preference is set
|
||||
await TryAutoSendWeeklyEmail(installation, report);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
|
|
@ -379,8 +382,8 @@ public static class ReportAggregationService
|
|||
InstallationId = installationId,
|
||||
Year = year,
|
||||
Month = month,
|
||||
PeriodStart = first.ToString("yyyy-MM-dd"),
|
||||
PeriodEnd = last.ToString("yyyy-MM-dd"),
|
||||
PeriodStart = days.Min(d => d.Date), // actual first data day, not calendar month start
|
||||
PeriodEnd = days.Max(d => d.Date), // actual last data day, not calendar month end
|
||||
TotalPvProduction = totalPv,
|
||||
TotalConsumption = totalConsump,
|
||||
TotalGridImport = totalGridIn,
|
||||
|
|
@ -416,6 +419,9 @@ public static class ReportAggregationService
|
|||
});
|
||||
|
||||
Console.WriteLine($"[ReportAggregation] Monthly report created for installation {installationId}, {year}-{month:D2} ({days.Count} days, {first}–{last}).");
|
||||
|
||||
// Auto-send email if preference is set
|
||||
await TryAutoSendMonthlyEmail(installationId, monthlySummary);
|
||||
}
|
||||
|
||||
// ── Year-End Aggregation ──────────────────────────────────────────
|
||||
|
|
@ -529,6 +535,82 @@ public static class ReportAggregationService
|
|||
});
|
||||
|
||||
Console.WriteLine($"[ReportAggregation] Yearly report created for installation {installationId}, {year} ({monthlies.Count} months aggregated).");
|
||||
|
||||
// Auto-send email if preference is set
|
||||
await TryAutoSendYearlyEmail(installationId, yearlySummary);
|
||||
}
|
||||
|
||||
// ── Auto-Send Email Helpers ─────────────────────────────────────────
|
||||
|
||||
private static async Task TryAutoSendWeeklyEmail(Installation installation, WeeklyReportResponse report)
|
||||
{
|
||||
try
|
||||
{
|
||||
var pref = Db.GetEmailPreference(installation.Id);
|
||||
if (pref is not { SendWeekly: true }) return;
|
||||
|
||||
var email = installation.Email;
|
||||
if (String.IsNullOrWhiteSpace(email))
|
||||
{
|
||||
Console.WriteLine($"[AutoSend] Weekly: skipping installation {installation.Id} — no email configured.");
|
||||
return;
|
||||
}
|
||||
|
||||
await ReportEmailService.SendReportEmailAsync(report, email, "en", installation.Name);
|
||||
Console.WriteLine($"[AutoSend] Weekly email sent to {email} for installation {installation.Id}.");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.Error.WriteLine($"[AutoSend] Weekly email failed for installation {installation.Id}: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
private static async Task TryAutoSendMonthlyEmail(Int64 installationId, MonthlyReportSummary report)
|
||||
{
|
||||
try
|
||||
{
|
||||
var pref = Db.GetEmailPreference(installationId);
|
||||
if (pref is not { SendMonthly: true }) return;
|
||||
|
||||
var installation = Db.GetInstallationById(installationId);
|
||||
var email = installation?.Email;
|
||||
if (String.IsNullOrWhiteSpace(email))
|
||||
{
|
||||
Console.WriteLine($"[AutoSend] Monthly: skipping installation {installationId} — no email configured.");
|
||||
return;
|
||||
}
|
||||
|
||||
await ReportEmailService.SendMonthlyReportEmailAsync(report, installation!.Name, email, "en");
|
||||
Console.WriteLine($"[AutoSend] Monthly email sent to {email} for installation {installationId}.");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.Error.WriteLine($"[AutoSend] Monthly email failed for installation {installationId}: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
private static async Task TryAutoSendYearlyEmail(Int64 installationId, YearlyReportSummary report)
|
||||
{
|
||||
try
|
||||
{
|
||||
var pref = Db.GetEmailPreference(installationId);
|
||||
if (pref is not { SendYearly: true }) return;
|
||||
|
||||
var installation = Db.GetInstallationById(installationId);
|
||||
var email = installation?.Email;
|
||||
if (String.IsNullOrWhiteSpace(email))
|
||||
{
|
||||
Console.WriteLine($"[AutoSend] Yearly: skipping installation {installationId} — no email configured.");
|
||||
return;
|
||||
}
|
||||
|
||||
await ReportEmailService.SendYearlyReportEmailAsync(report, installation!.Name, email, "en");
|
||||
Console.WriteLine($"[AutoSend] Yearly email sent to {email} for installation {installationId}.");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.Error.WriteLine($"[AutoSend] Yearly email failed for installation {installationId}: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
// ── AI Insight Cache ──────────────────────────────────────────────
|
||||
|
|
@ -799,4 +881,132 @@ Rules: Write in {langName}. Write for a homeowner. No asterisks or formatting ma
|
|||
|
||||
return "AI insight could not be generated at this time.";
|
||||
}
|
||||
|
||||
// ── Current-Period Previews (not saved to DB) ─────────────────────
|
||||
|
||||
public static async Task<MonthlyReportSummary?> GetCurrentMonthPreviewAsync(Int64 installationId, String language = "en")
|
||||
{
|
||||
var now = DateTime.UtcNow;
|
||||
var year = now.Year;
|
||||
var month = now.Month;
|
||||
var first = new DateOnly(year, month, 1);
|
||||
var last = first.AddMonths(1).AddDays(-1);
|
||||
var days = Db.GetDailyRecords(installationId, first, last);
|
||||
|
||||
if (days.Count == 0)
|
||||
return null;
|
||||
|
||||
var totalPv = Math.Round(days.Sum(d => d.PvProduction), 1);
|
||||
var totalConsump = Math.Round(days.Sum(d => d.LoadConsumption), 1);
|
||||
var totalGridIn = Math.Round(days.Sum(d => d.GridImport), 1);
|
||||
var totalGridOut = Math.Round(days.Sum(d => d.GridExport), 1);
|
||||
var totalBattChg = Math.Round(days.Sum(d => d.BatteryCharged), 1);
|
||||
var totalBattDis = Math.Round(days.Sum(d => d.BatteryDischarged), 1);
|
||||
|
||||
var energySaved = Math.Round(totalConsump - totalGridIn, 1);
|
||||
var savingsCHF = Math.Round(energySaved * ElectricityPriceCHF, 0);
|
||||
var selfSufficiency = totalConsump > 0 ? Math.Round((totalConsump - totalGridIn) / totalConsump * 100, 1) : 0;
|
||||
var selfConsumption = totalPv > 0 ? Math.Round((totalPv - totalGridOut) / totalPv * 100, 1) : 0;
|
||||
var batteryEff = totalBattChg > 0 ? Math.Round(totalBattDis / totalBattChg * 100, 1) : 0;
|
||||
var gridDependency = totalConsump > 0 ? Math.Round(totalGridIn / totalConsump * 100, 1) : 0;
|
||||
|
||||
var installation = Db.GetInstallationById(installationId);
|
||||
var installationName = installation?.Name ?? $"Installation {installationId}";
|
||||
var monthName = new DateTime(year, month, 1).ToString("MMMM yyyy");
|
||||
var weatherCity = !string.IsNullOrWhiteSpace(installation?.City) ? installation.City : installation?.Location;
|
||||
var weatherRegion = !string.IsNullOrWhiteSpace(installation?.Canton) ? installation.Canton : installation?.Region;
|
||||
|
||||
var aiInsight = await GenerateMonthlyAiInsightAsync(
|
||||
installationName, monthName, days.Count,
|
||||
totalPv, totalConsump, totalGridIn, totalGridOut,
|
||||
totalBattChg, totalBattDis, energySaved, savingsCHF,
|
||||
selfSufficiency, batteryEff, language,
|
||||
weatherCity, installation?.Country, weatherRegion);
|
||||
|
||||
var firstDataDay = days.Min(d => d.Date);
|
||||
var lastDataDay = days.Max(d => d.Date);
|
||||
|
||||
return new MonthlyReportSummary
|
||||
{
|
||||
InstallationId = installationId,
|
||||
Year = year,
|
||||
Month = month,
|
||||
PeriodStart = firstDataDay,
|
||||
PeriodEnd = lastDataDay,
|
||||
TotalPvProduction = totalPv,
|
||||
TotalConsumption = totalConsump,
|
||||
TotalGridImport = totalGridIn,
|
||||
TotalGridExport = totalGridOut,
|
||||
TotalBatteryCharged = totalBattChg,
|
||||
TotalBatteryDischarged = totalBattDis,
|
||||
TotalEnergySaved = energySaved,
|
||||
TotalSavingsCHF = savingsCHF,
|
||||
SelfSufficiencyPercent = selfSufficiency,
|
||||
SelfConsumptionPercent = selfConsumption,
|
||||
BatteryEfficiencyPercent = batteryEff,
|
||||
GridDependencyPercent = gridDependency,
|
||||
WeekCount = days.Count,
|
||||
AiInsight = aiInsight,
|
||||
CreatedAt = DateTime.UtcNow.ToString("o"),
|
||||
IsPreview = true,
|
||||
DaysAvailable = days.Count,
|
||||
DaysInMonth = last.Day,
|
||||
};
|
||||
}
|
||||
|
||||
public static async Task<YearlyReportSummary?> GetCurrentYearPreviewAsync(Int64 installationId, String language = "en")
|
||||
{
|
||||
var year = DateTime.UtcNow.Year;
|
||||
var monthlies = Db.GetMonthlyReportsForYear(installationId, year);
|
||||
|
||||
if (monthlies.Count == 0)
|
||||
return null;
|
||||
|
||||
var totalPv = Math.Round(monthlies.Sum(m => m.TotalPvProduction), 1);
|
||||
var totalConsump = Math.Round(monthlies.Sum(m => m.TotalConsumption), 1);
|
||||
var totalGridIn = Math.Round(monthlies.Sum(m => m.TotalGridImport), 1);
|
||||
var totalGridOut = Math.Round(monthlies.Sum(m => m.TotalGridExport), 1);
|
||||
var totalBattChg = Math.Round(monthlies.Sum(m => m.TotalBatteryCharged), 1);
|
||||
var totalBattDis = Math.Round(monthlies.Sum(m => m.TotalBatteryDischarged), 1);
|
||||
|
||||
var energySaved = Math.Round(totalConsump - totalGridIn, 1);
|
||||
var savingsCHF = Math.Round(energySaved * ElectricityPriceCHF, 0);
|
||||
var selfSufficiency = totalConsump > 0 ? Math.Round((totalConsump - totalGridIn) / totalConsump * 100, 1) : 0;
|
||||
var selfConsumption = totalPv > 0 ? Math.Round((totalPv - totalGridOut) / totalPv * 100, 1) : 0;
|
||||
var batteryEff = totalBattChg > 0 ? Math.Round(totalBattDis / totalBattChg * 100, 1) : 0;
|
||||
var gridDependency = totalConsump > 0 ? Math.Round(totalGridIn / totalConsump * 100, 1) : 0;
|
||||
|
||||
var installation = Db.GetInstallationById(installationId);
|
||||
var installationName = installation?.Name ?? $"Installation {installationId}";
|
||||
|
||||
var aiInsight = await GenerateYearlyAiInsightAsync(
|
||||
installationName, year, monthlies.Count,
|
||||
totalPv, totalConsump, totalGridIn, totalGridOut,
|
||||
totalBattChg, totalBattDis, energySaved, savingsCHF,
|
||||
selfSufficiency, batteryEff, language);
|
||||
|
||||
return new YearlyReportSummary
|
||||
{
|
||||
InstallationId = installationId,
|
||||
Year = year,
|
||||
PeriodStart = monthlies.Min(m => m.PeriodStart),
|
||||
PeriodEnd = monthlies.Max(m => m.PeriodEnd),
|
||||
TotalPvProduction = totalPv,
|
||||
TotalConsumption = totalConsump,
|
||||
TotalGridImport = totalGridIn,
|
||||
TotalGridExport = totalGridOut,
|
||||
TotalBatteryCharged = totalBattChg,
|
||||
TotalBatteryDischarged = totalBattDis,
|
||||
TotalEnergySaved = energySaved,
|
||||
TotalSavingsCHF = savingsCHF,
|
||||
SelfSufficiencyPercent = selfSufficiency,
|
||||
SelfConsumptionPercent = selfConsumption,
|
||||
BatteryEfficiencyPercent = batteryEff,
|
||||
GridDependencyPercent = gridDependency,
|
||||
MonthCount = monthlies.Count,
|
||||
AiInsight = aiInsight,
|
||||
CreatedAt = DateTime.UtcNow.ToString("o"),
|
||||
IsPreview = true,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
|
|
@ -40,6 +40,33 @@ public static class WeatherService
|
|||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns historical weather for a date range, or null on any failure.
|
||||
/// Uses Open-Meteo's archive API for past weather data.
|
||||
/// </summary>
|
||||
public static async Task<List<DailyWeather>?> GetHistoricalAsync(
|
||||
string? city, string? country, string? region,
|
||||
DateOnly startDate, DateOnly endDate)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(city))
|
||||
return null;
|
||||
|
||||
try
|
||||
{
|
||||
var coords = await GeocodeAsync(city, region);
|
||||
if (coords == null)
|
||||
return null;
|
||||
|
||||
var (lat, lon) = coords.Value;
|
||||
return await FetchHistoricalAsync(lat, lon, startDate, endDate);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.Error.WriteLine($"[WeatherService] Error fetching historical weather for '{city}': {ex.Message}");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Formats a forecast list into a compact text block for AI prompt injection.
|
||||
/// </summary>
|
||||
|
|
@ -52,7 +79,22 @@ public static class WeatherService
|
|||
return $"- {dayName}: {d.TempMin:F0}–{d.TempMax:F0}°C, {d.Description}, {d.SunshineHours:F1}h sunshine, {d.PrecipitationMm:F0}mm rain";
|
||||
});
|
||||
|
||||
return "WEATHER FORECAST (next 7 days):\n" + string.Join("\n", lines);
|
||||
return "WEATHER FORECAST (coming 7 days):\n" + string.Join("\n", lines);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Formats historical weather into a compact text block for AI prompt injection.
|
||||
/// </summary>
|
||||
public static string FormatHistoricalForPrompt(List<DailyWeather> historical)
|
||||
{
|
||||
var lines = historical.Select(d =>
|
||||
{
|
||||
var date = DateTime.Parse(d.Date);
|
||||
var dayName = date.ToString("ddd dd MMM");
|
||||
return $"- {dayName}: {d.TempMin:F0}–{d.TempMax:F0}°C, {d.Description}, {d.SunshineHours:F1}h sunshine, {d.PrecipitationMm:F0}mm rain";
|
||||
});
|
||||
|
||||
return "ACTUAL WEATHER (during reporting week):\n" + string.Join("\n", lines);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -145,6 +187,44 @@ public static class WeatherService
|
|||
return forecast;
|
||||
}
|
||||
|
||||
private static async Task<List<DailyWeather>?> FetchHistoricalAsync(double lat, double lon, DateOnly startDate, DateOnly endDate)
|
||||
{
|
||||
var url = $"https://archive-api.open-meteo.com/v1/archive"
|
||||
+ $"?latitude={lat}&longitude={lon}"
|
||||
+ $"&start_date={startDate:yyyy-MM-dd}&end_date={endDate:yyyy-MM-dd}"
|
||||
+ "&daily=temperature_2m_max,temperature_2m_min,sunshine_duration,precipitation_sum,weathercode"
|
||||
+ "&timezone=Europe/Zurich";
|
||||
|
||||
var json = await url.GetStringAsync();
|
||||
var data = JsonConvert.DeserializeObject<dynamic>(json);
|
||||
|
||||
if (data?.daily == null)
|
||||
return null;
|
||||
|
||||
var dates = data.daily.time;
|
||||
var tempMax = data.daily.temperature_2m_max;
|
||||
var tempMin = data.daily.temperature_2m_min;
|
||||
var sun = data.daily.sunshine_duration;
|
||||
var precip = data.daily.precipitation_sum;
|
||||
var codes = data.daily.weathercode;
|
||||
|
||||
var historical = new List<DailyWeather>();
|
||||
for (int i = 0; i < dates.Count; i++)
|
||||
{
|
||||
historical.Add(new DailyWeather(
|
||||
Date: (string)dates[i],
|
||||
TempMin: (double)tempMin[i],
|
||||
TempMax: (double)tempMax[i],
|
||||
SunshineHours: Math.Round((double)sun[i] / 3600.0, 1),
|
||||
PrecipitationMm: (double)precip[i],
|
||||
Description: WeatherCodeToDescription((int)codes[i])
|
||||
));
|
||||
}
|
||||
|
||||
Console.WriteLine($"[WeatherService] Fetched {historical.Count}-day historical weather ({startDate:yyyy-MM-dd} to {endDate:yyyy-MM-dd}).");
|
||||
return historical;
|
||||
}
|
||||
|
||||
private static string WeatherCodeToDescription(int code) => code switch
|
||||
{
|
||||
0 => "Clear sky",
|
||||
|
|
|
|||
|
|
@ -274,7 +274,8 @@ public static class WeeklyReportService
|
|||
var aiInsight = await GetAiInsightAsync(
|
||||
currentWeekDays, currentSummary, previousSummary,
|
||||
selfSufficiency, totalEnergySaved, totalSavingsCHF,
|
||||
behavior, installationName, language, location, country, region);
|
||||
behavior, installationName, language,
|
||||
weekStart, weekEnd, location, country, region);
|
||||
|
||||
// Compute data availability — which days of the week are missing
|
||||
var availableDates = currentWeekDays.Select(d => d.Date).ToHashSet();
|
||||
|
|
@ -356,6 +357,8 @@ public static class WeeklyReportService
|
|||
BehavioralPattern behavior,
|
||||
string installationName,
|
||||
string language = "en",
|
||||
DateOnly? periodStart = null,
|
||||
DateOnly? periodEnd = null,
|
||||
string? location = null,
|
||||
string? country = null,
|
||||
string? region = null)
|
||||
|
|
@ -367,7 +370,23 @@ public static class WeeklyReportService
|
|||
return "AI insight unavailable (API key not configured).";
|
||||
}
|
||||
|
||||
// Fetch weather forecast for the installation's location
|
||||
// Date labels for prompt clarity
|
||||
var today = DateOnly.FromDateTime(DateTime.UtcNow);
|
||||
var periodLabel = periodStart.HasValue && periodEnd.HasValue
|
||||
? $"{periodStart.Value:MMM dd}–{periodEnd.Value:MMM dd}"
|
||||
: "the reporting week";
|
||||
|
||||
// Fetch historical weather for the report week (actual conditions)
|
||||
List<WeatherService.DailyWeather>? historical = null;
|
||||
var historicalBlock = "";
|
||||
if (periodStart.HasValue && periodEnd.HasValue)
|
||||
{
|
||||
historical = await WeatherService.GetHistoricalAsync(location, country, region, periodStart.Value, periodEnd.Value);
|
||||
historicalBlock = historical != null ? "\n" + WeatherService.FormatHistoricalForPrompt(historical) + "\n" : "";
|
||||
Console.WriteLine($"[WeeklyReportService] Historical weather: {(historical != null ? $"{historical.Count} days fetched" : "SKIPPED (no location or API error)")}");
|
||||
}
|
||||
|
||||
// Fetch weather forecast for the coming week
|
||||
var forecast = await WeatherService.GetForecastAsync(location, country, region);
|
||||
var weatherBlock = forecast != null ? "\n" + WeatherService.FormatForPrompt(forecast) + "\n" : "";
|
||||
Console.WriteLine($"[WeeklyReportService] Weather forecast: {(forecast != null ? $"{forecast.Count} days fetched" : "SKIPPED (no location or API error)")}");
|
||||
|
|
@ -399,22 +418,29 @@ public static class WeeklyReportService
|
|||
var battDepleteLine = hasBattery
|
||||
? (behavior.AvgBatteryDepletedHour >= 0
|
||||
? $"Battery typically depletes below 20% during {FormatHourSlot(behavior.AvgBatteryDepletedHour)}."
|
||||
: "Battery stayed above 20% SoC every night this week.")
|
||||
: $"Battery stayed above 20% SoC every night during {periodLabel}.")
|
||||
: "";
|
||||
|
||||
var weekdayWeekendLine = behavior.WeekendAvgDailyLoad > 0
|
||||
? $"Weekday avg load: {behavior.WeekdayAvgDailyLoad} kWh/day. Weekend avg: {behavior.WeekendAvgDailyLoad} kWh/day."
|
||||
: $"Weekday avg load: {behavior.WeekdayAvgDailyLoad} kWh/day.";
|
||||
|
||||
// Look up actual weather for best/worst solar days (if historical data available)
|
||||
var bestDayWeather = historical?.FirstOrDefault(w => w.Date == bestDay.Date);
|
||||
var worstDayWeather = historical?.FirstOrDefault(w => w.Date == worstDay.Date);
|
||||
|
||||
var bestDayWeatherNote = bestDayWeather != null ? $" (actual weather: {bestDayWeather.Description}, {bestDayWeather.SunshineHours:F1}h sunshine)" : "";
|
||||
var worstDayWeatherNote = worstDayWeather != null ? $" (actual weather: {worstDayWeather.Description}, {worstDayWeather.SunshineHours:F1}h sunshine)" : "";
|
||||
|
||||
// Build conditional fact lines
|
||||
var pvDailyFact = hasPv
|
||||
? $"- PV: total {current.TotalPvProduction:F1} kWh this week. Best day: {bestDayName} ({bestDay.PvProduction:F1} kWh), worst: {worstDayName} ({worstDay.PvProduction:F1} kWh). Solar covered {selfSufficiency}% of consumption."
|
||||
? $"- PV: total {current.TotalPvProduction:F1} kWh for {periodLabel}. Best day: {bestDayName} ({bestDay.PvProduction:F1} kWh){bestDayWeatherNote}, worst: {worstDayName} ({worstDay.PvProduction:F1} kWh){worstDayWeatherNote}. Solar covered {selfSufficiency}% of consumption."
|
||||
: "";
|
||||
var battDailyFact = hasBattery
|
||||
? $"- Battery: {current.TotalBatteryCharged:F1} kWh charged, {current.TotalBatteryDischarged:F1} kWh discharged. Most active day: {topBattDayName} ({topBattDay.BatteryCharged:F1} kWh charged)."
|
||||
: "";
|
||||
var gridDailyFact = hasGrid
|
||||
? $"- Grid import: {current.TotalGridImport:F1} kWh total this week."
|
||||
? $"- Grid import: {current.TotalGridImport:F1} kWh total for {periodLabel}."
|
||||
: "";
|
||||
|
||||
// Behavioral section — only include when hourly data exists
|
||||
|
|
@ -432,7 +458,7 @@ public static class WeeklyReportService
|
|||
var battBehaviorLine = !string.IsNullOrEmpty(battDepleteLine) ? $"- {battDepleteLine}" : "";
|
||||
|
||||
behavioralSection = $@"
|
||||
BEHAVIORAL PATTERN (from hourly data this week):
|
||||
BEHAVIORAL PATTERN (from hourly data for {periodLabel}):
|
||||
- Peak household load: {FormatHourSlot(behavior.PeakLoadHour)}, avg {behavior.AvgPeakLoadKwh} kWh during that hour
|
||||
- {weekdayWeekendLine}{pvBehaviorLines}
|
||||
{gridBehaviorLine}
|
||||
|
|
@ -440,12 +466,15 @@ BEHAVIORAL PATTERN (from hourly data this week):
|
|||
}
|
||||
|
||||
// Build conditional instructions
|
||||
var instruction1 = $"1. Energy savings: Write 1–2 sentences. Say that this week, thanks to sodistore home, the customer avoided buying {totalEnergySaved} kWh from the grid, saving {totalSavingsCHF} CHF (at {ElectricityPriceCHF} CHF/kWh). Use these exact numbers — do not recalculate or change them.";
|
||||
var instruction1 = $"1. Energy savings: Write 1–2 sentences. Say that during {periodLabel}, thanks to sodistore home, the customer avoided buying {totalEnergySaved} kWh from the grid, saving {totalSavingsCHF} CHF (at {ElectricityPriceCHF} CHF/kWh). Use these exact numbers — do not recalculate or change them.";
|
||||
|
||||
var hasHistorical = historical != null && historical.Count > 0;
|
||||
var instruction2 = hasPv
|
||||
? $"2. Solar performance: Comment on the best and worst solar day this week and the likely weather reason."
|
||||
? hasHistorical
|
||||
? $"2. Solar performance: Comment on the best and worst solar day during {periodLabel}. Use the ACTUAL WEATHER data provided above to explain why — do NOT guess the weather. Reference the real conditions (sunshine hours, weather description) from the historical weather data."
|
||||
: $"2. Solar performance: Comment on the best and worst solar day during {periodLabel}. Only state the production numbers — do NOT speculate about weather reasons if no weather data is provided."
|
||||
: hasGrid
|
||||
? $"2. Grid usage: Comment on the {current.TotalGridImport:F1} kWh drawn from the grid this week."
|
||||
? $"2. Grid usage: Comment on the {current.TotalGridImport:F1} kWh drawn from the grid during {periodLabel}."
|
||||
: "2. Consumption pattern: Comment on the weekday vs weekend load pattern.";
|
||||
|
||||
var instruction3 = hasBattery
|
||||
|
|
@ -455,19 +484,24 @@ BEHAVIORAL PATTERN (from hourly data this week):
|
|||
// Instruction 4 — adapts based on whether we have behavioral data
|
||||
string instruction4;
|
||||
if (hasBehavior && hasPv)
|
||||
instruction4 = $"4. Smart action for next week: Write exactly 2 sentences. Sentence 1: point out the timing mismatch using exact numbers — peak household load is during {FormatHourSlot(behavior.PeakLoadHour)} ({behavior.AvgPeakLoadKwh} kWh) but solar peaks during {FormatHourSlot(behavior.PeakSolarHour)} ({behavior.AvgPeakSolarKwh} kWh), with solar active from {peakSolarWindow}. Sentence 2: suggest shifting energy-intensive appliances (such as washing machine, dishwasher, heat pump, or EV charger if applicable) to run during the solar window {peakSolarWindow} — do not assume which specific device the customer has.";
|
||||
instruction4 = $"4. Smart action for the coming week: Write exactly 2 sentences. Sentence 1: point out the timing mismatch using exact numbers — peak household load is during {FormatHourSlot(behavior.PeakLoadHour)} ({behavior.AvgPeakLoadKwh} kWh) but solar peaks during {FormatHourSlot(behavior.PeakSolarHour)} ({behavior.AvgPeakSolarKwh} kWh), with solar active from {peakSolarWindow}. Sentence 2: suggest shifting energy-intensive appliances (such as washing machine, dishwasher, heat pump, or EV charger if applicable) to run during the solar window {peakSolarWindow} — do not assume which specific device the customer has.";
|
||||
else if (hasBehavior && hasGrid)
|
||||
instruction4 = $"4. Smart action for next week: Write exactly 2 sentences. Sentence 1: state that the peak grid-import hour is {FormatHourSlot(behavior.HighestGridImportHour)} ({behavior.AvgGridImportAtPeakHour} kWh avg). Sentence 2: suggest one action to reduce grid use during that hour — shifting energy-intensive appliances (washing machine, dishwasher, heat pump, EV charger) away from that time.";
|
||||
instruction4 = $"4. Smart action for the coming week: Write exactly 2 sentences. Sentence 1: state that the peak grid-import hour is {FormatHourSlot(behavior.HighestGridImportHour)} ({behavior.AvgGridImportAtPeakHour} kWh avg). Sentence 2: suggest one action to reduce grid use during that hour — shifting energy-intensive appliances (washing machine, dishwasher, heat pump, EV charger) away from that time.";
|
||||
else
|
||||
instruction4 = "4. Smart action for next week: Based on this week's daily energy patterns, suggest one practical tip to maximize self-consumption and reduce grid dependency.";
|
||||
instruction4 = $"4. Smart action for the coming week: Based on the energy patterns from {periodLabel}, suggest one practical tip to maximize self-consumption and reduce grid dependency.";
|
||||
|
||||
// Instruction 5 — weather outlook with pattern-based predictions
|
||||
var hasWeather = forecast != null;
|
||||
var bulletCount = hasWeather ? 5 : 4;
|
||||
// Forecast date range label for prompt
|
||||
var forecastLabel = forecast != null && forecast.Count > 0
|
||||
? $"{DateTime.Parse(forecast.First().Date):MMM dd}–{DateTime.Parse(forecast.Last().Date):MMM dd}"
|
||||
: "the coming days";
|
||||
|
||||
var instruction5 = "";
|
||||
if (hasWeather && hasPv)
|
||||
{
|
||||
// Compute avg daily PV production this week for reference
|
||||
// Compute avg daily PV production for the reporting week as reference
|
||||
var avgDailyPv = currentWeek.Count > 0 ? Math.Round(currentWeek.Average(d => d.PvProduction), 1) : 0;
|
||||
var bestDayPv = Math.Round(bestDay.PvProduction, 1);
|
||||
var worstDayPv = Math.Round(worstDay.PvProduction, 1);
|
||||
|
|
@ -477,36 +511,39 @@ BEHAVIORAL PATTERN (from hourly data this week):
|
|||
var cloudyDays = forecast.Where(d => d.SunshineHours < 3).Select(d => DateTime.Parse(d.Date).ToString("dddd")).ToList();
|
||||
var totalForecastSunshine = Math.Round(forecast.Sum(d => d.SunshineHours), 1);
|
||||
|
||||
var patternContext = $"This week the installation averaged {avgDailyPv} kWh/day PV (best: {bestDayPv} kWh on {bestDayName}, worst: {worstDayPv} kWh on {worstDayName}). ";
|
||||
var patternContext = $"During {periodLabel} the installation averaged {avgDailyPv} kWh/day PV (best: {bestDayPv} kWh on {bestDayName}, worst: {worstDayPv} kWh on {worstDayName}). ";
|
||||
if (sunnyDays.Count > 0)
|
||||
patternContext += $"Next week, sunny days ({string.Join(", ", sunnyDays)}) should produce above average (~{avgDailyPv}+ kWh/day). ";
|
||||
patternContext += $"In the coming days ({forecastLabel}), sunny days ({string.Join(", ", sunnyDays)}) should produce above average (~{avgDailyPv}+ kWh/day). ";
|
||||
if (cloudyDays.Count > 0)
|
||||
patternContext += $"Cloudy/rainy days ({string.Join(", ", cloudyDays)}) will likely produce below average (~{worstDayPv} kWh/day or less). ";
|
||||
patternContext += $"Total forecast sunshine next week: {totalForecastSunshine}h.";
|
||||
patternContext += $"Total forecast sunshine for {forecastLabel}: {totalForecastSunshine}h.";
|
||||
|
||||
instruction5 = $@"5. Weather outlook: {patternContext} Write 2-3 concise sentences. (1) Name the best solar days next week and estimate production based on this week's pattern. (2) Recommend which specific days are best for running energy-heavy appliances (washing machine, dishwasher, EV charging) to maximize free solar energy. (3) If rainy days follow sunny days, suggest front-loading heavy usage onto the sunny days before the rain arrives. IMPORTANT: Do NOT suggest ""reducing consumption"" in the evening — people need electricity for cooking, lighting, and daily life. Instead, focus on SHIFTING discretionary loads (laundry, dishwasher, EV) to the best solar days. The battery system handles grid charging automatically — do not tell the customer to manage battery charging.";
|
||||
instruction5 = $@"5. Weather outlook: {patternContext} Write 2-3 concise sentences. (1) Name the best solar days in the coming days ({forecastLabel}) and estimate production based on the reporting week's pattern. (2) Recommend which specific days are best for running energy-heavy appliances (washing machine, dishwasher, EV charging) to maximize free solar energy. (3) If rainy days follow sunny days, suggest front-loading heavy usage onto the sunny days before the rain arrives. IMPORTANT: Do NOT suggest ""reducing consumption"" in the evening — people need electricity for cooking, lighting, and daily life. Instead, focus on SHIFTING discretionary loads (laundry, dishwasher, EV) to the best solar days. The battery system handles grid charging automatically — do not tell the customer to manage battery charging.";
|
||||
}
|
||||
else if (hasWeather)
|
||||
{
|
||||
instruction5 = @"5. Weather outlook: Summarize the coming week's weather in 1-2 sentences. Mention which days have the most sunshine and suggest those as the best days to run energy-heavy appliances (laundry, dishwasher). Do NOT suggest reducing evening consumption — focus on shifting discretionary loads to sunny days.";
|
||||
instruction5 = $@"5. Weather outlook: Summarize the weather for the coming days ({forecastLabel}) in 1-2 sentences. Mention which days have the most sunshine and suggest those as the best days to run energy-heavy appliances (laundry, dishwasher). Do NOT suggest reducing evening consumption — focus on shifting discretionary loads to sunny days.";
|
||||
}
|
||||
|
||||
var prompt = $@"You are an energy advisor for a sodistore home installation: ""{installationName}"".
|
||||
Today is {today:yyyy-MM-dd} ({today:dddd}). This report covers the week of {periodLabel}.
|
||||
|
||||
Write {bulletCount} 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.
|
||||
|
||||
CRITICAL: All numbers below are pre-calculated. Use these values as-is — do not recalculate, round differently, or change any number.
|
||||
CRITICAL: Use explicit date references. Say ""during {periodLabel}"" for the reporting week. Say ""the coming days ({forecastLabel})"" for the forecast period. NEVER use ambiguous terms like ""this week"" or ""next week"".
|
||||
|
||||
SYSTEM COMPONENTS: PV={hasPv}, Battery={hasBattery}, Grid={hasGrid}
|
||||
|
||||
DAILY FACTS:
|
||||
- Total consumption: {current.TotalConsumption:F1} kWh this week. Self-sufficiency: {selfSufficiency}%.
|
||||
DAILY FACTS (for {periodLabel}):
|
||||
- Total consumption: {current.TotalConsumption:F1} kWh. Self-sufficiency: {selfSufficiency}%.
|
||||
{pvDailyFact}
|
||||
{battDailyFact}
|
||||
{gridDailyFact}
|
||||
{behavioralSection}
|
||||
{historicalBlock}
|
||||
{weatherBlock}
|
||||
INSTRUCTIONS:
|
||||
{instruction1}
|
||||
|
|
|
|||
|
|
@ -20,8 +20,9 @@ export function formatPowerForGraph(value, magnitude): { value: number } {
|
|||
}
|
||||
}
|
||||
|
||||
const result = negative === false ? value : -value;
|
||||
return {
|
||||
value: negative === false ? value : -value
|
||||
value: Math.round(result * 100) / 100
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
Binary file not shown.
|
After Width: | Height: | Size: 64 KiB |
|
|
@ -1,117 +1,85 @@
|
|||
import { Step } from 'react-joyride';
|
||||
import { IntlShape } from 'react-intl';
|
||||
|
||||
// --- Build a single step with i18n ---
|
||||
// --- Tab key → i18n content description mapping ---
|
||||
// Only the *content* (description) needs i18n keys.
|
||||
// The *title* is read directly from the rendered tab element's text,
|
||||
// so it always matches the tab label in the current language.
|
||||
|
||||
function makeStep(
|
||||
intl: IntlShape,
|
||||
target: string,
|
||||
titleId: string,
|
||||
contentId: string,
|
||||
placement: Step['placement'] = 'bottom',
|
||||
disableBeacon = false
|
||||
): Step {
|
||||
return {
|
||||
target,
|
||||
title: intl.formatMessage({ id: titleId }),
|
||||
content: intl.formatMessage({ id: contentId }),
|
||||
placement,
|
||||
...(disableBeacon ? { disableBeacon: true } : {})
|
||||
};
|
||||
}
|
||||
|
||||
// --- Tab key → i18n key mapping ---
|
||||
|
||||
const tabConfig: Record<string, { titleId: string; contentId: string }> = {
|
||||
list: { titleId: 'tourListTitle', contentId: 'tourListContent' },
|
||||
tree: { titleId: 'tourTreeTitle', contentId: 'tourTreeContent' },
|
||||
live: { titleId: 'tourLiveTitle', contentId: 'tourLiveContent' },
|
||||
overview: { titleId: 'tourOverviewTitle', contentId: 'tourOverviewContent' },
|
||||
batteryview: { titleId: 'tourBatteryviewTitle', contentId: 'tourBatteryviewContent' },
|
||||
pvview: { titleId: 'tourPvviewTitle', contentId: 'tourPvviewContent' },
|
||||
log: { titleId: 'tourLogTitle', contentId: 'tourLogContent' },
|
||||
information: { titleId: 'tourInformationTitle', contentId: 'tourInformationContent' },
|
||||
report: { titleId: 'tourReportTitle', contentId: 'tourReportContent' },
|
||||
manage: { titleId: 'tourManageTitle', contentId: 'tourManageContent' },
|
||||
configuration: { titleId: 'tourConfigurationTitle', contentId: 'tourConfigurationContent' },
|
||||
history: { titleId: 'tourHistoryTitle', contentId: 'tourHistoryContent' }
|
||||
const tabContentKey: Record<string, string> = {
|
||||
list: 'tourListContent',
|
||||
tree: 'tourTreeContent',
|
||||
live: 'tourLiveContent',
|
||||
overview: 'tourOverviewContent',
|
||||
batteryview: 'tourBatteryviewContent',
|
||||
pvview: 'tourPvviewContent',
|
||||
log: 'tourLogContent',
|
||||
information: 'tourInformationContent',
|
||||
report: 'tourReportContent',
|
||||
manage: 'tourManageContent',
|
||||
configuration: 'tourConfigurationContent',
|
||||
history: 'tourHistoryContent',
|
||||
installationTickets: 'tourInstallationTicketsContent'
|
||||
};
|
||||
|
||||
// Steps to skip inside a specific installation (already covered in the list-page tour)
|
||||
const listPageOnlyTabs = new Set(['list', 'tree']);
|
||||
|
||||
// --- Build tour steps from tab value list ---
|
||||
|
||||
function buildTourSteps(intl: IntlShape, tabValues: string[], includeInstallationHint = false, isInsideInstallation = false): Step[] {
|
||||
/**
|
||||
* Build tour steps dynamically from the DOM.
|
||||
* Scans for all rendered `#tour-tab-*` elements and creates a step for each.
|
||||
* The step title is the tab's own rendered text (matching across languages).
|
||||
*/
|
||||
export function buildDynamicTourSteps(intl: IntlShape, isInsideInstallation: boolean): Step[] {
|
||||
const steps: Step[] = [];
|
||||
|
||||
// Language selector step (only on list/tree pages, not inside an installation)
|
||||
if (!isInsideInstallation) {
|
||||
steps.push(makeStep(intl, '[data-tour="language-selector"]', 'tourLanguageTitle', 'tourLanguageContent', 'bottom', true));
|
||||
}
|
||||
for (const value of tabValues) {
|
||||
if (isInsideInstallation && listPageOnlyTabs.has(value)) continue;
|
||||
const cfg = tabConfig[value];
|
||||
if (cfg) {
|
||||
steps.push(makeStep(intl, `#tour-tab-${value}`, cfg.titleId, cfg.contentId, 'bottom', steps.length === 0));
|
||||
const langEl = document.querySelector('[data-tour="language-selector"]');
|
||||
if (langEl) {
|
||||
steps.push({
|
||||
target: '[data-tour="language-selector"]',
|
||||
title: intl.formatMessage({ id: 'tourLanguageTitle' }),
|
||||
content: intl.formatMessage({ id: 'tourLanguageContent' }),
|
||||
placement: 'bottom',
|
||||
disableBeacon: true
|
||||
});
|
||||
}
|
||||
}
|
||||
if (includeInstallationHint && !isInsideInstallation) {
|
||||
steps.push(makeStep(intl, '#tour-tab-list', 'tourExploreTitle', 'tourExploreContent'));
|
||||
|
||||
// Collect all tour-tab elements in DOM order
|
||||
const tabEls = document.querySelectorAll<HTMLElement>('[id^="tour-tab-"]');
|
||||
tabEls.forEach((el) => {
|
||||
const rect = el.getBoundingClientRect();
|
||||
if (rect.width === 0 || rect.height === 0) return;
|
||||
|
||||
const tabValue = el.id.replace('tour-tab-', '');
|
||||
|
||||
// Skip list/tree tabs when inside an installation (already covered on list page)
|
||||
if (isInsideInstallation && (tabValue === 'list' || tabValue === 'tree')) return;
|
||||
|
||||
// Use the tab's own rendered text as title (matches current language)
|
||||
const title = el.textContent?.trim() || tabValue;
|
||||
const contentKey = tabContentKey[tabValue];
|
||||
const content = contentKey
|
||||
? intl.formatMessage({ id: contentKey })
|
||||
: '';
|
||||
|
||||
steps.push({
|
||||
target: `#tour-tab-${tabValue}`,
|
||||
title,
|
||||
content,
|
||||
placement: 'bottom' as const,
|
||||
...(steps.length === 0 ? { disableBeacon: true } : {})
|
||||
});
|
||||
});
|
||||
|
||||
// "Explore" hint at the end when on list/tree page
|
||||
if (!isInsideInstallation && document.querySelector('#tour-tab-list')) {
|
||||
steps.push({
|
||||
target: '#tour-tab-list',
|
||||
title: intl.formatMessage({ id: 'tourExploreTitle' }),
|
||||
content: intl.formatMessage({ id: 'tourExploreContent' }),
|
||||
placement: 'bottom'
|
||||
});
|
||||
}
|
||||
|
||||
return steps;
|
||||
}
|
||||
|
||||
// --- Sodistore Home (product 2) ---
|
||||
|
||||
export const buildSodiohomeCustomerTourSteps = (intl: IntlShape, inside = false) => buildTourSteps(intl, [
|
||||
'live', 'overview', 'information', 'report'
|
||||
], false, inside);
|
||||
|
||||
export const buildSodiohomePartnerTourSteps = (intl: IntlShape, inside = false) => buildTourSteps(intl, [
|
||||
'list', 'tree', 'live', 'overview', 'batteryview', 'log', 'information', 'report'
|
||||
], true, inside);
|
||||
|
||||
export const buildSodiohomeAdminTourSteps = (intl: IntlShape, inside = false) => buildTourSteps(intl, [
|
||||
'list', 'tree', 'live', 'overview', 'batteryview', 'log', 'manage', 'information', 'configuration', 'history', 'report'
|
||||
], true, inside);
|
||||
|
||||
// --- Salimax (product 0) / Sodistore Max (product 3) ---
|
||||
|
||||
export const buildSalimaxCustomerTourSteps = (intl: IntlShape, inside = false) => buildTourSteps(intl, [
|
||||
'live', 'overview', 'information'
|
||||
], false, inside);
|
||||
|
||||
export const buildSalimaxPartnerTourSteps = (intl: IntlShape, inside = false) => buildTourSteps(intl, [
|
||||
'list', 'tree', 'live', 'overview', 'batteryview', 'pvview', 'information'
|
||||
], true, inside);
|
||||
|
||||
export const buildSalimaxAdminTourSteps = (intl: IntlShape, inside = false) => buildTourSteps(intl, [
|
||||
'list', 'tree', 'live', 'overview', 'batteryview', 'manage', 'log', 'information', 'configuration', 'history', 'pvview'
|
||||
], true, inside);
|
||||
|
||||
// --- Sodistore Grid (product 4) — same as Salimax but no PV View ---
|
||||
|
||||
export const buildSodistoregridCustomerTourSteps = (intl: IntlShape, inside = false) => buildTourSteps(intl, [
|
||||
'live', 'overview', 'information'
|
||||
], false, inside);
|
||||
|
||||
export const buildSodistoregridPartnerTourSteps = (intl: IntlShape, inside = false) => buildTourSteps(intl, [
|
||||
'list', 'tree', 'live', 'overview', 'batteryview', 'information'
|
||||
], true, inside);
|
||||
|
||||
export const buildSodistoregridAdminTourSteps = (intl: IntlShape, inside = false) => buildTourSteps(intl, [
|
||||
'list', 'tree', 'live', 'overview', 'batteryview', 'manage', 'log', 'information', 'configuration', 'history'
|
||||
], true, inside);
|
||||
|
||||
// --- Salidomo (product 1) ---
|
||||
|
||||
export const buildSalidomoCustomerTourSteps = (intl: IntlShape, inside = false) => buildTourSteps(intl, [
|
||||
'batteryview', 'overview', 'information'
|
||||
], false, inside);
|
||||
|
||||
export const buildSalidomoPartnerTourSteps = (intl: IntlShape, inside = false) => buildTourSteps(intl, [
|
||||
'list', 'tree', 'batteryview', 'overview', 'information'
|
||||
], true, inside);
|
||||
|
||||
export const buildSalidomoAdminTourSteps = (intl: IntlShape, inside = false) => buildTourSteps(intl, [
|
||||
'list', 'tree', 'batteryview', 'overview', 'log', 'manage', 'information', 'history'
|
||||
], true, inside);
|
||||
|
|
|
|||
|
|
@ -56,7 +56,6 @@ function BatteryViewSodioHome(props: BatteryViewSodioHomeProps) {
|
|||
Current: device?.[`Battery${batteryIndex}PackTotalCurrent`] ?? 0,
|
||||
Power: device?.[`Battery${batteryIndex}Power`] ?? 0,
|
||||
Soc: device?.[`Battery${batteryIndex}Soc`] ?? device?.[`Battery${batteryIndex}SocSecondvalue`] ?? 0,
|
||||
Soh: device?.[`Battery${batteryIndex}Soh`] ?? 0,
|
||||
}
|
||||
};
|
||||
} else {
|
||||
|
|
@ -69,7 +68,6 @@ function BatteryViewSodioHome(props: BatteryViewSodioHomeProps) {
|
|||
Current: inverter[`Battery${index}Current`] ?? 0,
|
||||
Power: inverter[`Battery${index}Power`] ?? 0,
|
||||
Soc: inverter[`Battery${index}Soc`] ?? 0,
|
||||
Soh: inverter[`Battery${index}Soh`] ?? 0,
|
||||
}
|
||||
};
|
||||
}
|
||||
|
|
@ -221,7 +219,7 @@ function BatteryViewSodioHome(props: BatteryViewSodioHomeProps) {
|
|||
<TableCell align="center"><FormattedMessage id="batteryVoltage" defaultMessage="Battery Voltage" /></TableCell>
|
||||
<TableCell align="center"><FormattedMessage id="current" defaultMessage="Current" /></TableCell>
|
||||
<TableCell align="center"><FormattedMessage id="soc" defaultMessage="SoC" /></TableCell>
|
||||
<TableCell align="center"><FormattedMessage id="soh" defaultMessage="SoH" /></TableCell>
|
||||
{/*<TableCell align="center"><FormattedMessage id="soh" defaultMessage="SoH" /></TableCell>*/}
|
||||
{/*<TableCell align="center">Daily Charge Energy</TableCell>*/}
|
||||
{/*<TableCell align="center">Daily Discharge Energy</TableCell>*/}
|
||||
</TableRow>
|
||||
|
|
@ -294,7 +292,7 @@ function BatteryViewSodioHome(props: BatteryViewSodioHomeProps) {
|
|||
>
|
||||
{battery.Soc + ' %'}
|
||||
</TableCell>
|
||||
<TableCell
|
||||
{/*<TableCell
|
||||
sx={{
|
||||
width: '8%',
|
||||
textAlign: 'center',
|
||||
|
|
@ -308,7 +306,7 @@ function BatteryViewSodioHome(props: BatteryViewSodioHomeProps) {
|
|||
}}
|
||||
>
|
||||
{battery.Soh + ' %'}
|
||||
</TableCell>
|
||||
</TableCell>*/}
|
||||
{/*<TableCell*/}
|
||||
{/* sx={{*/}
|
||||
{/* width: '15%',*/}
|
||||
|
|
|
|||
|
|
@ -754,63 +754,7 @@ function MainStatsSodioHome(props: MainStatsSodioHomeProps) {
|
|||
</Card>
|
||||
</Grid>
|
||||
|
||||
{/* Battery SoH Chart */}
|
||||
<Grid item md={12} xs={12}>
|
||||
<Card
|
||||
sx={{
|
||||
overflow: 'visible',
|
||||
marginTop: '10px',
|
||||
marginBottom: '30px'
|
||||
}}
|
||||
>
|
||||
<Box sx={{ marginLeft: '20px' }}>
|
||||
<Box display="flex" alignItems="center">
|
||||
<Box>
|
||||
<Typography variant="subtitle1" noWrap>
|
||||
<FormattedMessage
|
||||
id="battery_soh"
|
||||
defaultMessage="Battery SOH (State Of Health)"
|
||||
/>
|
||||
</Typography>
|
||||
</Box>
|
||||
</Box>
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'flex-start',
|
||||
pt: 3
|
||||
}}
|
||||
></Box>
|
||||
</Box>
|
||||
|
||||
<ReactApexChart
|
||||
options={{
|
||||
...getChartOptions(
|
||||
batteryViewDataArray[chartState].chartOverview.Soh,
|
||||
'daily',
|
||||
[],
|
||||
true
|
||||
),
|
||||
chart: {
|
||||
events: {
|
||||
beforeZoom: (chartContext, options) => {
|
||||
startZoom();
|
||||
handleBeforeZoom(chartContext, options);
|
||||
}
|
||||
}
|
||||
}
|
||||
}}
|
||||
series={generateSeries(
|
||||
batteryViewDataArray[chartState].chartData,
|
||||
'Soh',
|
||||
'green'
|
||||
)}
|
||||
type="line"
|
||||
height={420}
|
||||
/>
|
||||
</Card>
|
||||
</Grid>
|
||||
{/* Battery SoH Chart — removed for SodiostoreHome */}
|
||||
</Grid>
|
||||
</>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -686,7 +686,7 @@ function InformationSodistorehome(props: InformationSodistorehomeProps) {
|
|||
onInputChange={(_e, val) =>
|
||||
setFormValues({ ...formValues, networkProvider: val || '' })
|
||||
}
|
||||
disabled={!canEdit && !isPartner}
|
||||
disabled={!canEdit}
|
||||
loading={loadingProviders}
|
||||
renderInput={(params) => (
|
||||
<TextField
|
||||
|
|
@ -708,6 +708,18 @@ function InformationSodistorehome(props: InformationSodistorehomeProps) {
|
|||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<TextField
|
||||
label={<FormattedMessage id="emailAddress" defaultMessage="Email Address" />}
|
||||
name="email"
|
||||
value={formValues.email || ''}
|
||||
onChange={handleChange}
|
||||
variant="outlined"
|
||||
fullWidth
|
||||
inputProps={{ readOnly: !canEdit }}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<FormControl fullWidth sx={{ marginLeft: 1, marginTop: 1, marginBottom: 1, width: 440 }}>
|
||||
<InputLabel sx={{ fontSize: 14, backgroundColor: 'white' }}>
|
||||
|
|
@ -724,7 +736,7 @@ function InformationSodistorehome(props: InformationSodistorehomeProps) {
|
|||
const val = e.target.value as string;
|
||||
setFormValues({ ...formValues, externalEms: val });
|
||||
}}
|
||||
inputProps={{ readOnly: !canEdit && !isPartner }}
|
||||
inputProps={{ readOnly: !canEdit }}
|
||||
>
|
||||
<MenuItem value="No"><FormattedMessage id="emsNo" defaultMessage="No" /></MenuItem>
|
||||
<MenuItem value="Solar Manager">Solar Manager</MenuItem>
|
||||
|
|
@ -745,7 +757,7 @@ function InformationSodistorehome(props: InformationSodistorehomeProps) {
|
|||
onChange={handleChange}
|
||||
variant="outlined"
|
||||
fullWidth
|
||||
inputProps={{ readOnly: !canEdit && !isPartner }}
|
||||
inputProps={{ readOnly: !canEdit }}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -150,7 +150,8 @@ function Installation(props: singleInstallationProps) {
|
|||
return false;
|
||||
}
|
||||
console.log(`Timestamp: ${timestamp}`);
|
||||
console.log(res[timestamp]);
|
||||
const { Config: { S3: { Key, Secret, ...s3Rest } = {} as any, ...configRest } = {} as any, ...dataRest } = res[timestamp] || {};
|
||||
console.log({ ...dataRest, Config: { ...configRest, S3: { ...s3Rest, Key: '***', Secret: '***' } } });
|
||||
|
||||
setValues(res[timestamp]);
|
||||
await timeout(2000);
|
||||
|
|
@ -381,7 +382,7 @@ function Installation(props: singleInstallationProps) {
|
|||
{loading &&
|
||||
currentTab != 'information' &&
|
||||
currentTab != 'history' &&
|
||||
currentTab != 'manage' &&
|
||||
// currentTab != 'manage' &&
|
||||
currentTab != 'log' &&
|
||||
currentTab != 'installationTickets' && (
|
||||
<Container
|
||||
|
|
@ -538,7 +539,7 @@ function Installation(props: singleInstallationProps) {
|
|||
/>
|
||||
)}
|
||||
|
||||
{currentUser.userType == UserType.admin && (
|
||||
{/* {currentUser.userType == UserType.admin && (
|
||||
<Route
|
||||
path={routes.manage}
|
||||
element={
|
||||
|
|
@ -550,7 +551,7 @@ function Installation(props: singleInstallationProps) {
|
|||
</AccessContextProvider>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
)} */}
|
||||
|
||||
{currentUser.userType == UserType.admin && (
|
||||
<Route
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ function InstallationTabs(props: InstallationTabsProps) {
|
|||
const tabList = [
|
||||
'live',
|
||||
'overview',
|
||||
'manage',
|
||||
// 'manage',
|
||||
'batteryview',
|
||||
'log',
|
||||
'information',
|
||||
|
|
@ -125,15 +125,15 @@ function InstallationTabs(props: InstallationTabsProps) {
|
|||
)
|
||||
},
|
||||
|
||||
{
|
||||
value: 'manage',
|
||||
label: (
|
||||
<FormattedMessage
|
||||
id="manage"
|
||||
defaultMessage="Access Management"
|
||||
/>
|
||||
)
|
||||
},
|
||||
// {
|
||||
// value: 'manage',
|
||||
// label: (
|
||||
// <FormattedMessage
|
||||
// id="manage"
|
||||
// defaultMessage="Access Management"
|
||||
// />
|
||||
// )
|
||||
// },
|
||||
{
|
||||
value: 'log',
|
||||
label: <FormattedMessage id="log" defaultMessage="Log" />
|
||||
|
|
@ -259,15 +259,15 @@ function InstallationTabs(props: InstallationTabsProps) {
|
|||
value: 'pvview',
|
||||
label: <FormattedMessage id="pvview" defaultMessage="Pv View" />
|
||||
},
|
||||
{
|
||||
value: 'manage',
|
||||
label: (
|
||||
<FormattedMessage
|
||||
id="manage"
|
||||
defaultMessage="Access Management"
|
||||
/>
|
||||
)
|
||||
},
|
||||
// {
|
||||
// value: 'manage',
|
||||
// label: (
|
||||
// <FormattedMessage
|
||||
// id="manage"
|
||||
// defaultMessage="Access Management"
|
||||
// />
|
||||
// )
|
||||
// },
|
||||
{
|
||||
value: 'log',
|
||||
label: <FormattedMessage id="log" defaultMessage="Log" />
|
||||
|
|
|
|||
|
|
@ -3,10 +3,12 @@ import React, {
|
|||
useCallback,
|
||||
useContext,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useState
|
||||
} from 'react';
|
||||
import {
|
||||
Alert,
|
||||
Autocomplete,
|
||||
Box,
|
||||
Container,
|
||||
Divider,
|
||||
|
|
@ -19,6 +21,7 @@ import {
|
|||
MenuItem,
|
||||
Modal,
|
||||
Select,
|
||||
TextField,
|
||||
Typography,
|
||||
useTheme
|
||||
} from '@mui/material';
|
||||
|
|
@ -41,6 +44,16 @@ import {
|
|||
} from '../../../interfaces/InstallationTypes';
|
||||
import axiosConfig from '../../../Resources/axiosConfig';
|
||||
|
||||
const PRODUCT_GROUP_ORDER: number[] = [2, 5, 4, 3, 0, 1];
|
||||
const PRODUCT_NAMES: Record<number, string> = {
|
||||
0: 'Salimax',
|
||||
1: 'Salidomo',
|
||||
2: 'Sodistore Home',
|
||||
3: 'Sodistore Max',
|
||||
4: 'Sodistore Grid',
|
||||
5: 'Sodistore Pro'
|
||||
};
|
||||
|
||||
interface UserAccessProps {
|
||||
current_user: InnovEnergyUser;
|
||||
}
|
||||
|
|
@ -57,16 +70,25 @@ function UserAccess(props: UserAccessProps) {
|
|||
const context = useContext(UserContext);
|
||||
const { currentUser } = context;
|
||||
const [openFolder, setOpenFolder] = useState(false);
|
||||
const [openInstallation, setOpenInstallation] = useState(false);
|
||||
const [openModal, setOpenModal] = useState(false);
|
||||
|
||||
const [selectedFolderNames, setSelectedFolderNames] = useState<string[]>([]);
|
||||
const [selectedInstallationNames, setSelectedInstallationNames] = useState<string[]>([]);
|
||||
const [selectedInstallations, setSelectedInstallations] = useState<I_Installation[]>([]);
|
||||
|
||||
// Available choices for grant modal
|
||||
const [availableFolders, setAvailableFolders] = useState<I_Folder[]>([]);
|
||||
const [availableInstallations, setAvailableInstallations] = useState<I_Installation[]>([]);
|
||||
|
||||
const sortedInstallations = useMemo(() => {
|
||||
const orderMap = new Map(PRODUCT_GROUP_ORDER.map((p, i) => [p, i]));
|
||||
const sorted = [...availableInstallations].sort((a, b) => {
|
||||
const oa = orderMap.get(a.product) ?? 99;
|
||||
const ob = orderMap.get(b.product) ?? 99;
|
||||
return oa !== ob ? oa - ob : a.name.localeCompare(b.name);
|
||||
});
|
||||
return sorted;
|
||||
}, [availableInstallations]);
|
||||
|
||||
// Direct grants for this user
|
||||
const [directFolders, setDirectFolders] = useState<{ id: number; name: string }[]>([]);
|
||||
const [directInstallations, setDirectInstallations] = useState<{ id: number; name: string }[]>([]);
|
||||
|
|
@ -107,15 +129,14 @@ function UserAccess(props: UserAccessProps) {
|
|||
|
||||
const fetchAvailableInstallations = useCallback(async () => {
|
||||
try {
|
||||
const [res0, res1, res2, res3, res4, res5] = await Promise.all([
|
||||
axiosConfig.get(`/GetAllInstallationsFromProduct?product=0`),
|
||||
axiosConfig.get(`/GetAllInstallationsFromProduct?product=1`),
|
||||
axiosConfig.get(`/GetAllInstallationsFromProduct?product=2`),
|
||||
axiosConfig.get(`/GetAllInstallationsFromProduct?product=3`),
|
||||
axiosConfig.get(`/GetAllInstallationsFromProduct?product=4`),
|
||||
axiosConfig.get(`/GetAllInstallationsFromProduct?product=5`)
|
||||
]);
|
||||
setAvailableInstallations([...res0.data, ...res1.data, ...res2.data, ...res3.data, ...res4.data, ...res5.data]);
|
||||
const products = [0, 1, 2, 3, 4, 5];
|
||||
const responses = await Promise.all(
|
||||
products.map((p) => axiosConfig.get(`/GetAllInstallationsFromProduct?product=${p}`))
|
||||
);
|
||||
const all = responses.flatMap((res, idx) =>
|
||||
res.data.map((inst: I_Installation) => ({ ...inst, product: products[idx] }))
|
||||
);
|
||||
setAvailableInstallations(all);
|
||||
} catch (err) {
|
||||
if (err.response && err.response.status === 401) removeToken();
|
||||
}
|
||||
|
|
@ -130,7 +151,7 @@ function UserAccess(props: UserAccessProps) {
|
|||
fetchAvailableInstallations();
|
||||
setOpenModal(true);
|
||||
setSelectedFolderNames([]);
|
||||
setSelectedInstallationNames([]);
|
||||
setSelectedInstallations([]);
|
||||
};
|
||||
|
||||
const handleRevokeFolder = async (folderId: number, folderName: string) => {
|
||||
|
|
@ -178,8 +199,7 @@ function UserAccess(props: UserAccessProps) {
|
|||
});
|
||||
}
|
||||
|
||||
for (const installationName of selectedInstallationNames) {
|
||||
const installation = availableInstallations.find((i) => i.name === installationName);
|
||||
for (const installation of selectedInstallations) {
|
||||
await axiosConfig
|
||||
.post(`/GrantUserAccessToInstallation?UserId=${props.current_user.id}&InstallationId=${installation.id}`)
|
||||
.then(() => {
|
||||
|
|
@ -264,31 +284,43 @@ function UserAccess(props: UserAccessProps) {
|
|||
</div>
|
||||
|
||||
<div>
|
||||
<FormControl fullWidth sx={{ marginTop: 2, width: 390 }}>
|
||||
<InputLabel sx={{ fontSize: 14, backgroundColor: 'white' }}>
|
||||
<FormattedMessage id="grantAccessToInstallations" defaultMessage="Grant access to installations" />
|
||||
</InputLabel>
|
||||
<Select
|
||||
<FormControl fullWidth sx={{ marginTop: 1, width: 390 }}>
|
||||
<Autocomplete<I_Installation, true, false, false>
|
||||
multiple
|
||||
value={selectedInstallationNames}
|
||||
onChange={(e) => setSelectedInstallationNames(e.target.value as string[])}
|
||||
open={openInstallation}
|
||||
onClose={() => setOpenInstallation(false)}
|
||||
onOpen={() => setOpenInstallation(true)}
|
||||
renderValue={(selected) => (
|
||||
<div>{selected.map((i) => <span key={i}>{i}, </span>)}</div>
|
||||
options={sortedInstallations}
|
||||
groupBy={(option) => PRODUCT_NAMES[option.product] || 'Unknown'}
|
||||
getOptionLabel={(option) => option.name}
|
||||
value={selectedInstallations}
|
||||
onChange={(_event, newValue) => setSelectedInstallations(newValue)}
|
||||
isOptionEqualToValue={(option, value) => option.id === value.id}
|
||||
renderGroup={(params) => (
|
||||
<li key={params.key}>
|
||||
<Typography
|
||||
sx={{
|
||||
fontWeight: 'bold',
|
||||
fontSize: 13,
|
||||
padding: '4px 16px',
|
||||
backgroundColor: theme.colors.alpha.black[5],
|
||||
color: theme.colors.alpha.black[70]
|
||||
}}
|
||||
>
|
||||
{params.group}
|
||||
</Typography>
|
||||
<ul style={{ padding: 0 }}>{params.children}</ul>
|
||||
</li>
|
||||
)}
|
||||
>
|
||||
{availableInstallations.map((installation) => (
|
||||
<MenuItem key={installation.id} value={installation.name}>{installation.name}</MenuItem>
|
||||
))}
|
||||
<Button
|
||||
sx={{ marginLeft: '150px', marginTop: '10px', backgroundColor: theme.colors.primary.main, color: 'white', '&:hover': { backgroundColor: theme.colors.primary.dark }, padding: '6px 8px' }}
|
||||
onClick={() => setOpenInstallation(false)}
|
||||
>
|
||||
<FormattedMessage id="submit" defaultMessage="Submit" />
|
||||
</Button>
|
||||
</Select>
|
||||
renderInput={(params) => (
|
||||
<TextField
|
||||
{...params}
|
||||
label={intl.formatMessage({ id: 'grantAccessToInstallations' })}
|
||||
placeholder={intl.formatMessage({ id: 'searchInstallations' })}
|
||||
InputLabelProps={{
|
||||
...params.InputLabelProps,
|
||||
sx: { fontSize: 14, backgroundColor: 'white' }
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</FormControl>
|
||||
</div>
|
||||
|
||||
|
|
|
|||
|
|
@ -32,6 +32,7 @@ export const getChartOptions = (
|
|||
colors: ['#3498db', '#2ecc71', '#282828'],
|
||||
xaxis: {
|
||||
type: 'datetime',
|
||||
tickAmount: 8,
|
||||
labels: {
|
||||
datetimeFormatter: {
|
||||
year: 'yyyy',
|
||||
|
|
@ -51,6 +52,7 @@ export const getChartOptions = (
|
|||
? [
|
||||
{
|
||||
seriesName: 'Grid Power',
|
||||
tickAmount: 6,
|
||||
min:
|
||||
chartInfo.min >= 0
|
||||
? 0
|
||||
|
|
@ -88,6 +90,7 @@ export const getChartOptions = (
|
|||
{
|
||||
seriesName: 'Grid Power',
|
||||
show: false,
|
||||
tickAmount: 6,
|
||||
min:
|
||||
chartInfo.min >= 0
|
||||
? 0
|
||||
|
|
@ -104,15 +107,6 @@ export const getChartOptions = (
|
|||
: chartInfo.max <= 0
|
||||
? 0
|
||||
: undefined,
|
||||
title: {
|
||||
text: chartInfo.unit,
|
||||
style: {
|
||||
fontSize: '12px'
|
||||
},
|
||||
offsetY: -190,
|
||||
offsetX: 25,
|
||||
rotate: 0
|
||||
},
|
||||
labels: {
|
||||
formatter: function (value: number) {
|
||||
return formatPowerForGraph(
|
||||
|
|
@ -122,11 +116,39 @@ export const getChartOptions = (
|
|||
}
|
||||
}
|
||||
},
|
||||
|
||||
{
|
||||
seriesName: 'State Of Charge',
|
||||
seriesName: 'Grid Power',
|
||||
show: false,
|
||||
tickAmount: 6,
|
||||
min:
|
||||
chartInfo.min >= 0
|
||||
? 0
|
||||
: chartInfo.max <= 0
|
||||
? Math.ceil(
|
||||
chartInfo.min / findPower(chartInfo.min).value
|
||||
) * findPower(chartInfo.min).value
|
||||
: undefined,
|
||||
max:
|
||||
chartInfo.min >= 0
|
||||
? Math.ceil(
|
||||
chartInfo.max / findPower(chartInfo.max).value
|
||||
) * findPower(chartInfo.max).value
|
||||
: chartInfo.max <= 0
|
||||
? 0
|
||||
: undefined,
|
||||
labels: {
|
||||
formatter: function (value: number) {
|
||||
return formatPowerForGraph(
|
||||
value,
|
||||
chartInfo.magnitude
|
||||
).value.toString();
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
seriesName: 'Battery SOC',
|
||||
opposite: true,
|
||||
|
||||
tickAmount: 5,
|
||||
min: 0,
|
||||
max: 100,
|
||||
title: {
|
||||
|
|
@ -140,12 +162,13 @@ export const getChartOptions = (
|
|||
},
|
||||
labels: {
|
||||
formatter: function (value: number) {
|
||||
return formatPowerForGraph(value, 0).value.toString();
|
||||
return Math.round(value).toString();
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
: {
|
||||
tickAmount: chartInfo.unit === '(%)' ? 5 : 6,
|
||||
min:
|
||||
chartInfo.min >= 0
|
||||
? 0
|
||||
|
|
@ -173,6 +196,9 @@ export const getChartOptions = (
|
|||
},
|
||||
labels: {
|
||||
formatter: function (value: number) {
|
||||
if (chartInfo.unit === '(%)') {
|
||||
return Math.round(value).toString();
|
||||
}
|
||||
return formatPowerForGraph(
|
||||
value,
|
||||
chartInfo.magnitude
|
||||
|
|
@ -189,7 +215,7 @@ export const getChartOptions = (
|
|||
y: {
|
||||
formatter: function (val, { seriesIndex, w }) {
|
||||
const seriesName = w.config.series[seriesIndex].name;
|
||||
if (seriesName === 'State Of Charge') {
|
||||
if (seriesName === 'Battery SOC') {
|
||||
return val.toFixed(2) + ' %';
|
||||
} else {
|
||||
return (
|
||||
|
|
@ -255,6 +281,7 @@ export const getChartOptions = (
|
|||
}
|
||||
},
|
||||
yaxis: {
|
||||
tickAmount: 6,
|
||||
min:
|
||||
chartInfo.min >= 0
|
||||
? 0
|
||||
|
|
|
|||
|
|
@ -735,6 +735,11 @@ function Overview(props: OverviewProps) {
|
|||
type: 'line',
|
||||
color: '#ff9900'
|
||||
},
|
||||
{
|
||||
...dailyDataArray[chartState].chartData.ACLoad,
|
||||
type: 'line',
|
||||
color: '#2ecc71'
|
||||
},
|
||||
{
|
||||
...dailyDataArray[chartState].chartData.soc,
|
||||
type: 'line',
|
||||
|
|
|
|||
|
|
@ -105,7 +105,7 @@ function SalidomoInstallation(props: singleInstallationProps) {
|
|||
setLoading(false);
|
||||
console.log('NUMBER OF FILES=' + Object.keys(res).length);
|
||||
|
||||
console.log('res=', res);
|
||||
console.log('res= [S3 credentials hidden]');
|
||||
|
||||
while (continueFetching.current) {
|
||||
for (const timestamp of Object.keys(res)) {
|
||||
|
|
@ -114,7 +114,8 @@ function SalidomoInstallation(props: singleInstallationProps) {
|
|||
return false;
|
||||
}
|
||||
console.log(`Timestamp: ${timestamp}`);
|
||||
console.log('object is', res);
|
||||
const { Config: { S3: { Key, Secret, ...s3Rest } = {} as any, ...configRest } = {} as any, ...dataRest } = res[timestamp] || {};
|
||||
console.log('object is', { ...dataRest, Config: { ...configRest, S3: { ...s3Rest, Key: '***', Secret: '***' } } });
|
||||
|
||||
// Set values asynchronously with delay
|
||||
setValues(res[timestamp]);
|
||||
|
|
@ -323,7 +324,7 @@ function SalidomoInstallation(props: singleInstallationProps) {
|
|||
</div>
|
||||
{loading &&
|
||||
currentTab != 'information' &&
|
||||
currentTab != 'manage' &&
|
||||
// currentTab != 'manage' &&
|
||||
currentTab != 'history' &&
|
||||
currentTab != 'log' &&
|
||||
currentTab != 'installationTickets' && (
|
||||
|
|
@ -416,7 +417,7 @@ function SalidomoInstallation(props: singleInstallationProps) {
|
|||
/>
|
||||
)}
|
||||
|
||||
{currentUser.userType == UserType.admin && (
|
||||
{/* {currentUser.userType == UserType.admin && (
|
||||
<Route
|
||||
path={routes.manage}
|
||||
element={
|
||||
|
|
@ -428,7 +429,7 @@ function SalidomoInstallation(props: singleInstallationProps) {
|
|||
</AccessContextProvider>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
)} */}
|
||||
|
||||
{currentUser.userType == UserType.admin && (
|
||||
<Route
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ function SalidomoInstallationTabs(props: InstallationTabsProps) {
|
|||
const tabList = [
|
||||
'batteryview',
|
||||
'information',
|
||||
'manage',
|
||||
// 'manage',
|
||||
'overview',
|
||||
'log',
|
||||
'history',
|
||||
|
|
@ -113,15 +113,15 @@ function SalidomoInstallationTabs(props: InstallationTabsProps) {
|
|||
label: <FormattedMessage id="log" defaultMessage="Log" />
|
||||
},
|
||||
|
||||
{
|
||||
value: 'manage',
|
||||
label: (
|
||||
<FormattedMessage
|
||||
id="manage"
|
||||
defaultMessage="Access Management"
|
||||
/>
|
||||
)
|
||||
},
|
||||
// {
|
||||
// value: 'manage',
|
||||
// label: (
|
||||
// <FormattedMessage
|
||||
// id="manage"
|
||||
// defaultMessage="Access Management"
|
||||
// />
|
||||
// )
|
||||
// },
|
||||
|
||||
{
|
||||
value: 'information',
|
||||
|
|
@ -198,15 +198,15 @@ function SalidomoInstallationTabs(props: InstallationTabsProps) {
|
|||
label: <FormattedMessage id="log" defaultMessage="Log" />
|
||||
},
|
||||
|
||||
{
|
||||
value: 'manage',
|
||||
label: (
|
||||
<FormattedMessage
|
||||
id="manage"
|
||||
defaultMessage="Access Management"
|
||||
/>
|
||||
)
|
||||
},
|
||||
// {
|
||||
// value: 'manage',
|
||||
// label: (
|
||||
// <FormattedMessage
|
||||
// id="manage"
|
||||
// defaultMessage="Access Management"
|
||||
// />
|
||||
// )
|
||||
// },
|
||||
|
||||
{
|
||||
value: 'information',
|
||||
|
|
|
|||
|
|
@ -56,19 +56,6 @@ interface HourlyEnergyRecord {
|
|||
|
||||
// ── Date Helpers ─────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Returns the Monday of the current week.
|
||||
*/
|
||||
function getCurrentMonday(): Date {
|
||||
const today = new Date();
|
||||
today.setHours(0, 0, 0, 0);
|
||||
const dow = today.getDay(); // 0=Sun
|
||||
const offset = dow === 0 ? 6 : dow - 1; // Mon=0 offset
|
||||
const monday = new Date(today);
|
||||
monday.setDate(today.getDate() - offset);
|
||||
return monday;
|
||||
}
|
||||
|
||||
function formatDateISO(d: Date): string {
|
||||
const y = d.getFullYear();
|
||||
const m = String(d.getMonth() + 1).padStart(2, '0');
|
||||
|
|
@ -77,19 +64,20 @@ function formatDateISO(d: Date): string {
|
|||
}
|
||||
|
||||
/**
|
||||
* Returns current week Mon→yesterday. Today excluded because
|
||||
* S3 aggregated file is not available until end of day.
|
||||
* Returns the last 7 days ending yesterday.
|
||||
* Today is excluded because S3 aggregated file is not available until ~01:00 UTC the next day.
|
||||
*/
|
||||
function getCurrentWeekDays(currentMonday: Date): Date[] {
|
||||
function getLast7Days(): Date[] {
|
||||
const yesterday = new Date();
|
||||
yesterday.setHours(0, 0, 0, 0);
|
||||
yesterday.setDate(yesterday.getDate() - 1);
|
||||
|
||||
const days: Date[] = [];
|
||||
|
||||
for (let d = new Date(currentMonday); d <= yesterday; d.setDate(d.getDate() + 1)) {
|
||||
days.push(new Date(d));
|
||||
for (let i = 6; i >= 0; i--) {
|
||||
const d = new Date(yesterday);
|
||||
d.setDate(yesterday.getDate() - i);
|
||||
days.push(d);
|
||||
}
|
||||
|
||||
return days;
|
||||
}
|
||||
|
||||
|
|
@ -105,7 +93,6 @@ export default function DailySection({
|
|||
onPeriodChange?: (date: string) => void;
|
||||
}) {
|
||||
const intl = useIntl();
|
||||
const currentMonday = useMemo(() => getCurrentMonday(), []);
|
||||
const yesterday = useMemo(() => {
|
||||
const d = new Date();
|
||||
d.setHours(0, 0, 0, 0);
|
||||
|
|
@ -125,11 +112,8 @@ export default function DailySection({
|
|||
const [loadingWeek, setLoadingWeek] = useState(false);
|
||||
const [noData, setNoData] = useState(false);
|
||||
|
||||
// Current week Mon→yesterday only
|
||||
const weekDays = useMemo(
|
||||
() => getCurrentWeekDays(currentMonday),
|
||||
[currentMonday]
|
||||
);
|
||||
// Rolling 7-day window ending yesterday
|
||||
const weekDays = useMemo(() => getLast7Days(), []);
|
||||
|
||||
// Fetch data for current week days
|
||||
useEffect(() => {
|
||||
|
|
@ -193,7 +177,7 @@ export default function DailySection({
|
|||
|
||||
return (
|
||||
<>
|
||||
{/* Day Strip — current week Mon→yesterday */}
|
||||
{/* Day Strip — last 7 days ending yesterday */}
|
||||
<DayStrip
|
||||
weekDays={weekDays}
|
||||
selectedDate={selectedDate}
|
||||
|
|
@ -344,7 +328,7 @@ function DayStrip({
|
|||
<Typography variant="caption" sx={{ color: '#888' }}>
|
||||
<FormattedMessage
|
||||
id="currentWeekHint"
|
||||
defaultMessage="Current week (Mon–yesterday)"
|
||||
defaultMessage="Last 7 days"
|
||||
/>
|
||||
</Typography>
|
||||
</Box>
|
||||
|
|
@ -353,9 +337,10 @@ function DayStrip({
|
|||
|
||||
// ── IntradayChart ────────────────────────────────────────────
|
||||
|
||||
const HOUR_LABELS = Array.from({ length: 24 }, (_, i) =>
|
||||
`${String(i).padStart(2, '0')}:00`
|
||||
);
|
||||
const HOUR_LABELS = [
|
||||
...Array.from({ length: 24 }, (_, i) => `${String(i).padStart(2, '0')}:00`),
|
||||
'24:00'
|
||||
];
|
||||
|
||||
function IntradayChart({
|
||||
hourlyData,
|
||||
|
|
@ -387,13 +372,14 @@ function IntradayChart({
|
|||
|
||||
const hourMap = new Map(hourlyData.map((h) => [h.hour, h]));
|
||||
|
||||
const pvData = HOUR_LABELS.map((_, i) => hourMap.get(i)?.pvKwh ?? null);
|
||||
const loadData = HOUR_LABELS.map((_, i) => hourMap.get(i)?.loadKwh ?? null);
|
||||
const getHour = (i: number) => hourMap.get(i === 24 ? 23 : i);
|
||||
const pvData = HOUR_LABELS.map((_, i) => getHour(i)?.pvKwh ?? null);
|
||||
const loadData = HOUR_LABELS.map((_, i) => getHour(i)?.loadKwh ?? null);
|
||||
const batteryData = HOUR_LABELS.map((_, i) => {
|
||||
const h = hourMap.get(i);
|
||||
const h = getHour(i);
|
||||
return h ? h.batteryDischargedKwh - h.batteryChargedKwh : null;
|
||||
});
|
||||
const socData = HOUR_LABELS.map((_, i) => hourMap.get(i)?.battSoC ?? null);
|
||||
const socData = HOUR_LABELS.map((_, i) => getHour(i)?.battSoC ?? null);
|
||||
|
||||
const chartData = {
|
||||
labels: HOUR_LABELS,
|
||||
|
|
|
|||
|
|
@ -183,7 +183,8 @@ function SodioHomeInstallation(props: singleInstallationProps) {
|
|||
return false;
|
||||
}
|
||||
console.log(`Timestamp: ${timestamp}`);
|
||||
console.log(res[timestamp]);
|
||||
const { Config: { S3: { Key, Secret, ...s3Rest } = {} as any, ...configRest } = {} as any, ...dataRest } = res[timestamp] || {};
|
||||
console.log({ ...dataRest, Config: { ...configRest, S3: { ...s3Rest, Key: '***', Secret: '***' } } });
|
||||
|
||||
setValues(res[timestamp]);
|
||||
await timeout(2000);
|
||||
|
|
@ -473,7 +474,7 @@ function SodioHomeInstallation(props: singleInstallationProps) {
|
|||
</div>
|
||||
{loading &&
|
||||
currentTab != 'information' &&
|
||||
currentTab != 'manage' &&
|
||||
// currentTab != 'manage' &&
|
||||
currentTab != 'history' &&
|
||||
currentTab != 'log' &&
|
||||
currentTab != 'report' &&
|
||||
|
|
@ -584,7 +585,7 @@ function SodioHomeInstallation(props: singleInstallationProps) {
|
|||
/>
|
||||
)}
|
||||
|
||||
{currentUser.userType == UserType.admin && (
|
||||
{/* {currentUser.userType == UserType.admin && (
|
||||
<Route
|
||||
path={routes.manage}
|
||||
element={
|
||||
|
|
@ -596,7 +597,7 @@ function SodioHomeInstallation(props: singleInstallationProps) {
|
|||
</AccessContextProvider>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
)} */}
|
||||
|
||||
<Route
|
||||
path={routes.overview}
|
||||
|
|
@ -618,6 +619,8 @@ function SodioHomeInstallation(props: singleInstallationProps) {
|
|||
element={
|
||||
<WeeklyReport
|
||||
installationId={props.current_installation.id}
|
||||
installationName={props.current_installation.name}
|
||||
installationEmail={props.current_installation.email}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
|
|
@ -47,7 +47,7 @@ function SodioHomeInstallationTabs(props: SodioHomeInstallationTabsProps) {
|
|||
'overview',
|
||||
'batteryview',
|
||||
'information',
|
||||
'manage',
|
||||
// 'manage',
|
||||
'log',
|
||||
'history',
|
||||
'configuration',
|
||||
|
|
@ -142,15 +142,15 @@ function SodioHomeInstallationTabs(props: SodioHomeInstallationTabsProps) {
|
|||
value: 'log',
|
||||
label: <FormattedMessage id="log" defaultMessage="Log" />
|
||||
},
|
||||
{
|
||||
value: 'manage',
|
||||
label: (
|
||||
<FormattedMessage
|
||||
id="manage"
|
||||
defaultMessage="Access Management"
|
||||
/>
|
||||
)
|
||||
},
|
||||
// {
|
||||
// value: 'manage',
|
||||
// label: (
|
||||
// <FormattedMessage
|
||||
// id="manage"
|
||||
// defaultMessage="Access Management"
|
||||
// />
|
||||
// )
|
||||
// },
|
||||
{
|
||||
value: 'information',
|
||||
label: (
|
||||
|
|
@ -297,15 +297,15 @@ function SodioHomeInstallationTabs(props: SodioHomeInstallationTabsProps) {
|
|||
value: 'log',
|
||||
label: <FormattedMessage id="log" defaultMessage="Log" />
|
||||
},
|
||||
{
|
||||
value: 'manage',
|
||||
label: (
|
||||
<FormattedMessage
|
||||
id="manage"
|
||||
defaultMessage="Access Management"
|
||||
/>
|
||||
)
|
||||
},
|
||||
// {
|
||||
// value: 'manage',
|
||||
// label: (
|
||||
// <FormattedMessage
|
||||
// id="manage"
|
||||
// defaultMessage="Access Management"
|
||||
// />
|
||||
// )
|
||||
// },
|
||||
{
|
||||
value: 'information',
|
||||
label: (
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import React, { useCallback, useContext, useEffect, useState } from 'react';
|
||||
import React, { useCallback, useContext, useEffect, useMemo, useState } from 'react';
|
||||
import {
|
||||
Alert,
|
||||
Autocomplete,
|
||||
Box,
|
||||
CircularProgress,
|
||||
FormControl,
|
||||
|
|
@ -10,6 +11,7 @@ import {
|
|||
Modal,
|
||||
Select,
|
||||
TextField,
|
||||
Typography,
|
||||
useTheme
|
||||
} from '@mui/material';
|
||||
import Button from '@mui/material/Button';
|
||||
|
|
@ -20,6 +22,16 @@ import { TokenContext } from 'src/contexts/tokenContext';
|
|||
import { I_Folder, I_Installation } from 'src/interfaces/InstallationTypes';
|
||||
import { FormattedMessage, useIntl } from 'react-intl';
|
||||
|
||||
const PRODUCT_GROUP_ORDER: number[] = [2, 5, 4, 3, 0, 1];
|
||||
const PRODUCT_NAMES: Record<number, string> = {
|
||||
0: 'Salimax',
|
||||
1: 'Salidomo',
|
||||
2: 'Sodistore Home',
|
||||
3: 'Sodistore Max',
|
||||
4: 'Sodistore Grid',
|
||||
5: 'Sodistore Pro'
|
||||
};
|
||||
|
||||
interface userFormProps {
|
||||
cancel: () => void;
|
||||
submit: () => void;
|
||||
|
|
@ -32,7 +44,6 @@ function userForm(props: userFormProps) {
|
|||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState(false);
|
||||
const [errormessage, setErrorMessage] = useState(intl.formatMessage({ id: 'errorOccured' }));
|
||||
const [openInstallation, setOpenInstallation] = useState(false);
|
||||
const [openFolder, setOpenFolder] = useState(false);
|
||||
const [formValues, setFormValues] = useState<Partial<InnovEnergyUser>>({
|
||||
name: '',
|
||||
|
|
@ -41,9 +52,7 @@ function userForm(props: userFormProps) {
|
|||
});
|
||||
const requiredFields = ['name', 'email'];
|
||||
const [selectedFolderNames, setSelectedFolderNames] = useState<string[]>([]);
|
||||
const [selectedInstallationNames, setSelectedInstallationNames] = useState<
|
||||
string[]
|
||||
>([]);
|
||||
const [selectedInstallations, setSelectedInstallations] = useState<I_Installation[]>([]);
|
||||
|
||||
const UserTypes = ['Client', 'Partner', 'Admin'];
|
||||
|
||||
|
|
@ -72,23 +81,13 @@ function userForm(props: userFormProps) {
|
|||
setLoading(true);
|
||||
|
||||
try {
|
||||
const [res0, res1, res2, res3, res4, res5] = await Promise.all([
|
||||
axiosConfig.get(`/GetAllInstallationsFromProduct?product=0`),
|
||||
axiosConfig.get(`/GetAllInstallationsFromProduct?product=1`),
|
||||
axiosConfig.get(`/GetAllInstallationsFromProduct?product=2`),
|
||||
axiosConfig.get(`/GetAllInstallationsFromProduct?product=3`),
|
||||
axiosConfig.get(`/GetAllInstallationsFromProduct?product=4`),
|
||||
axiosConfig.get(`/GetAllInstallationsFromProduct?product=5`)
|
||||
]);
|
||||
|
||||
const combined = [
|
||||
...res0.data,
|
||||
...res1.data,
|
||||
...res2.data,
|
||||
...res3.data,
|
||||
...res4.data,
|
||||
...res5.data
|
||||
];
|
||||
const products = [0, 1, 2, 3, 4, 5];
|
||||
const responses = await Promise.all(
|
||||
products.map((p) => axiosConfig.get(`/GetAllInstallationsFromProduct?product=${p}`))
|
||||
);
|
||||
const combined = responses.flatMap((res, idx) =>
|
||||
res.data.map((inst: I_Installation) => ({ ...inst, product: products[idx] }))
|
||||
);
|
||||
|
||||
setInstallations(combined);
|
||||
} catch (err) {
|
||||
|
|
@ -100,6 +99,15 @@ function userForm(props: userFormProps) {
|
|||
}
|
||||
}, [setInstallations]);
|
||||
|
||||
const sortedInstallations = useMemo(() => {
|
||||
const orderMap = new Map(PRODUCT_GROUP_ORDER.map((p, i) => [p, i]));
|
||||
return [...installations].sort((a, b) => {
|
||||
const oa = orderMap.get(a.product) ?? 99;
|
||||
const ob = orderMap.get(b.product) ?? 99;
|
||||
return oa !== ob ? oa - ob : a.name.localeCompare(b.name);
|
||||
});
|
||||
}, [installations]);
|
||||
|
||||
useEffect(() => {
|
||||
fetchFolders();
|
||||
fetchInstallations();
|
||||
|
|
@ -116,10 +124,6 @@ function userForm(props: userFormProps) {
|
|||
setSelectedFolderNames(event.target.value);
|
||||
};
|
||||
|
||||
const handleInstallationChange = (event) => {
|
||||
setSelectedInstallationNames(event.target.value);
|
||||
};
|
||||
|
||||
const isMobile = window.innerWidth <= 1490;
|
||||
|
||||
const handleSubmit = async (e) => {
|
||||
|
|
@ -153,11 +157,7 @@ function userForm(props: userFormProps) {
|
|||
});
|
||||
}
|
||||
|
||||
for (const installationName of selectedInstallationNames) {
|
||||
const installation = installations.find(
|
||||
(installation) => installation.name === installationName
|
||||
);
|
||||
|
||||
for (const installation of selectedInstallations) {
|
||||
await axiosConfig
|
||||
.post(
|
||||
`/GrantUserAccessToInstallation?UserId=${res.data.id}&InstallationId=${installation.id}`
|
||||
|
|
@ -207,14 +207,6 @@ function userForm(props: userFormProps) {
|
|||
});
|
||||
};
|
||||
|
||||
const handleOpenInstallation = () => {
|
||||
setOpenInstallation(true);
|
||||
};
|
||||
|
||||
const handleCloseInstallation = () => {
|
||||
setOpenInstallation(false);
|
||||
};
|
||||
|
||||
const handleOpenFolder = () => {
|
||||
setOpenFolder(true);
|
||||
};
|
||||
|
|
@ -345,55 +337,43 @@ function userForm(props: userFormProps) {
|
|||
</div>
|
||||
|
||||
<div>
|
||||
<FormControl fullWidth sx={{ marginTop: 2, width: 390 }}>
|
||||
<InputLabel
|
||||
sx={{
|
||||
fontSize: 14,
|
||||
backgroundColor: 'white'
|
||||
}}
|
||||
>
|
||||
<FormattedMessage
|
||||
id="grantAccessToInstallations"
|
||||
defaultMessage="Grant access to installations"
|
||||
/>
|
||||
</InputLabel>
|
||||
<Select
|
||||
multiple
|
||||
value={selectedInstallationNames}
|
||||
onChange={handleInstallationChange}
|
||||
open={openInstallation}
|
||||
onClose={handleCloseInstallation}
|
||||
onOpen={handleOpenInstallation}
|
||||
renderValue={(selected) => (
|
||||
<div>
|
||||
{selected.map((installation) => (
|
||||
<span key={installation}>{installation}, </span>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
>
|
||||
{installations.map((installation) => (
|
||||
<MenuItem key={installation.id} value={installation.name}>
|
||||
{installation.name}
|
||||
</MenuItem>
|
||||
))}
|
||||
<Button
|
||||
sx={{
|
||||
marginLeft: '150px',
|
||||
marginTop: '10px',
|
||||
backgroundColor: theme.colors.primary.main,
|
||||
color: 'white',
|
||||
'&:hover': {
|
||||
backgroundColor: theme.colors.primary.dark
|
||||
},
|
||||
padding: '6px 8px'
|
||||
<Autocomplete<I_Installation, true, false, false>
|
||||
multiple
|
||||
options={sortedInstallations}
|
||||
groupBy={(option) => PRODUCT_NAMES[option.product] || 'Unknown'}
|
||||
getOptionLabel={(option) => option.name}
|
||||
value={selectedInstallations}
|
||||
onChange={(_event, newValue) => setSelectedInstallations(newValue)}
|
||||
isOptionEqualToValue={(option, value) => option.id === value.id}
|
||||
renderGroup={(params) => (
|
||||
<li key={params.key}>
|
||||
<Typography
|
||||
sx={{
|
||||
fontWeight: 'bold',
|
||||
fontSize: 13,
|
||||
padding: '4px 16px',
|
||||
backgroundColor: theme.colors.alpha.black[5],
|
||||
color: theme.colors.alpha.black[70]
|
||||
}}
|
||||
>
|
||||
{params.group}
|
||||
</Typography>
|
||||
<ul style={{ padding: 0 }}>{params.children}</ul>
|
||||
</li>
|
||||
)}
|
||||
renderInput={(params) => (
|
||||
<TextField
|
||||
{...params}
|
||||
label={intl.formatMessage({ id: 'grantAccessToInstallations' })}
|
||||
placeholder={intl.formatMessage({ id: 'searchInstallations' })}
|
||||
InputLabelProps={{
|
||||
...params.InputLabelProps,
|
||||
sx: { fontSize: 14, backgroundColor: 'white' }
|
||||
}}
|
||||
onClick={handleCloseInstallation}
|
||||
>
|
||||
<FormattedMessage id="submit" defaultMessage="Submit" />
|
||||
</Button>
|
||||
</Select>
|
||||
</FormControl>
|
||||
/>
|
||||
)}
|
||||
sx={{ mt: 1 }}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
|
|
|
|||
|
|
@ -452,11 +452,11 @@ export const transformInputToDailyDataJson = async (
|
|||
];
|
||||
|
||||
const chartData: chartDataInterface = {
|
||||
soc: { name: 'State Of Charge', data: [] },
|
||||
soc: { name: 'Battery SOC', data: [] },
|
||||
temperature: { name: 'Battery Temperature', data: [] },
|
||||
dcPower: { name: 'Battery Power', data: [] },
|
||||
gridPower: { name: 'Grid Power', data: [] },
|
||||
pvProduction: { name: 'Pv Production', data: [] },
|
||||
pvProduction: { name: 'PV Power', data: [] },
|
||||
dcBusVoltage: { name: 'DC Bus Voltage', data: [] },
|
||||
ACLoad: { name: 'AC Load', data: [] },
|
||||
DCLoad: { name: 'DC Load', data: [] }
|
||||
|
|
@ -530,7 +530,8 @@ export const transformInputToDailyDataJson = async (
|
|||
Object.keys(results[i]).length - 1
|
||||
];
|
||||
const result = results[i][timestamp];
|
||||
//console.log(result);
|
||||
const { Config: { S3: { Key: _k, Secret: _s, ...s3Rest } = {} as any, ...configRest } = {} as any, ...dataRest } = result || {};
|
||||
console.log('Overview data:', { ...dataRest, Config: { ...configRest, S3: { ...s3Rest, Key: '***', Secret: '***' } } });
|
||||
let category_index = 0;
|
||||
// eslint-disable-next-line @typescript-eslint/no-loop-func
|
||||
pathsToSearch.forEach((path) => {
|
||||
|
|
@ -639,16 +640,19 @@ export const transformInputToDailyDataJson = async (
|
|||
chartOverview.overview = {
|
||||
magnitude: Math.max(
|
||||
chartOverview['gridPower'].magnitude,
|
||||
chartOverview['pvProduction'].magnitude
|
||||
chartOverview['pvProduction'].magnitude,
|
||||
chartOverview['ACLoad'].magnitude
|
||||
),
|
||||
unit: '(kW)',
|
||||
min: Math.min(
|
||||
chartOverview['gridPower'].min,
|
||||
chartOverview['pvProduction'].min
|
||||
chartOverview['pvProduction'].min,
|
||||
chartOverview['ACLoad'].min
|
||||
),
|
||||
max: Math.max(
|
||||
chartOverview['gridPower'].max,
|
||||
chartOverview['pvProduction'].max
|
||||
chartOverview['pvProduction'].max,
|
||||
chartOverview['ACLoad'].max
|
||||
)
|
||||
};
|
||||
|
||||
|
|
@ -673,13 +677,14 @@ const fetchJsonDataForOneTime = async (
|
|||
res = await fetchDataJson(timestampToFetch, s3Credentials);
|
||||
|
||||
if (res !== FetchResult.notAvailable && res !== FetchResult.tryLater) {
|
||||
//console.log('Successfully fetched ' + timestampToFetch);
|
||||
console.log('Successfully fetched ' + timestampToFetch);
|
||||
return res;
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Error fetching data:', err);
|
||||
}
|
||||
}
|
||||
console.warn('Failed to fetch timestamp ' + startUnixTime.ticks);
|
||||
return null;
|
||||
};
|
||||
|
||||
|
|
@ -771,7 +776,7 @@ export const transformInputToAggregatedDataJson = async (
|
|||
}
|
||||
|
||||
const results = await Promise.all(timestampPromises);
|
||||
console.log("Fetched aggregated daily results:", results);
|
||||
console.log("Fetched aggregated daily results: [count=" + results.length + "]");
|
||||
currentDay = start_date;
|
||||
|
||||
for (let i = 0; i < results.length; i++) {
|
||||
|
|
|
|||
|
|
@ -39,6 +39,7 @@ export interface I_Installation extends I_S3Credentials {
|
|||
status?: number;
|
||||
serialNumber?: string;
|
||||
networkProvider: string;
|
||||
email: string;
|
||||
}
|
||||
|
||||
export interface I_Folder {
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@
|
|||
"inverterFirmwareVersion": "Wechselrichter-Firmware-Version",
|
||||
"batteryFirmwareVersion": "Batterie-Firmware-Version",
|
||||
"networkProvider": "Netzbetreiber",
|
||||
"emailAddress": "E-Mail-Adresse",
|
||||
"createNewFolder": "Neuer Ordner",
|
||||
"createNewUser": "Neuer Benutzer",
|
||||
"customerName": "Kundenname",
|
||||
|
|
@ -119,6 +120,7 @@
|
|||
"deleteFolder": "Ordner löschen",
|
||||
"grantAccessToFolders": "Zugriff auf Ordner gewähren",
|
||||
"grantAccessToInstallations": "Zugriff auf Installationen gewähren",
|
||||
"searchInstallations": "Installationen suchen...",
|
||||
"cannotloadloggingdata": "Log Daten können nicht geladen werden",
|
||||
"grantedAccessToUsers": "Den Benutzern wurde den Zugriff gewährt",
|
||||
"unableToGrantAccess": "Der Zugriff kann nicht gewährt werden",
|
||||
|
|
@ -131,7 +133,6 @@
|
|||
"reportTitle": "Wöchentlicher Leistungsbericht",
|
||||
"weeklyInsights": "Wöchentliche Einblicke",
|
||||
"missingDaysWarning": "Daten verfügbar für {available}/{expected} Tage. Fehlend: {dates}",
|
||||
"weeklySavings": "Ihre Einsparungen diese Woche",
|
||||
"solarEnergyUsed": "Energie gespart",
|
||||
"solarStayedHome": "Solar + Batterie, nicht vom Netz",
|
||||
"daysOfYourUsage": "Tage Ihres Verbrauchs",
|
||||
|
|
@ -139,10 +140,8 @@
|
|||
"atCHFRate": "bei 0,39 CHF/kWh Ø",
|
||||
"solarCoverage": "Energieunabhängigkeit",
|
||||
"fromSolarSub": "aus eigenem Solar + Batterie System",
|
||||
"avgDailyConsumption": "Ø Tagesverbrauch",
|
||||
"batteryEfficiency": "Batterieeffizienz",
|
||||
"batteryEffSub": "Entladung vs. Ladung",
|
||||
"weeklySummary": "Wöchentliche Zusammenfassung",
|
||||
"metric": "Kennzahl",
|
||||
"thisWeek": "Diese Woche",
|
||||
"change": "Änderung",
|
||||
|
|
@ -152,11 +151,12 @@
|
|||
"gridExport": "Netzeinspeisung",
|
||||
"batteryInOut": "Batterie Laden / Entladen",
|
||||
"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.",
|
||||
"refreshReport": "Bericht aktualisieren",
|
||||
"refreshing": "Aktualisierung...",
|
||||
"ok": "Ok",
|
||||
"grantedAccessToUser": "Zugriff für Benutzer {name} gewährt",
|
||||
"proceed": "Fortfahren",
|
||||
|
|
@ -175,7 +175,7 @@
|
|||
"dailySummary": "Tagesübersicht",
|
||||
"noDataForDate": "Keine Daten für das gewählte Datum verfügbar.",
|
||||
"noHourlyData": "Stündliche Daten für diesen Tag nicht verfügbar.",
|
||||
"currentWeekHint": "Aktuelle Woche (Mo–gestern)",
|
||||
"currentWeekHint": "Letzte 7 Tage",
|
||||
"intradayChart": "Tagesverlauf Energiefluss",
|
||||
"batteryPower": "Batterieleistung",
|
||||
"batterySoCLabel": "Batterie SoC",
|
||||
|
|
@ -189,21 +189,20 @@
|
|||
"yearlyReportTitle": "Jährlicher Leistungsbericht",
|
||||
"monthlyInsights": "Monatliche Einblicke",
|
||||
"yearlyInsights": "Jährliche Einblicke",
|
||||
"monthlySavings": "Ihre Einsparungen diesen Monat",
|
||||
"yearlySavings": "Ihre Einsparungen dieses Jahr",
|
||||
"monthlySummary": "Monatliche Zusammenfassung",
|
||||
"yearlySummary": "Jährliche Zusammenfassung",
|
||||
"total": "Gesamt",
|
||||
"weeksAggregated": "{count} Wochen aggregiert",
|
||||
"monthsAggregated": "{count} Monate aggregiert",
|
||||
"noMonthlyData": "Noch keine monatlichen Berichte verfügbar. Wöchentliche Berichte erscheinen hier zur Aggregation.",
|
||||
"noYearlyData": "Noch keine jährlichen Berichte verfügbar. Monatliche Berichte erscheinen hier zur Aggregation.",
|
||||
"availableForGeneration": "Zur Generierung verfügbar",
|
||||
"generateMonth": "{month} {year} generieren ({count} Wochen)",
|
||||
"generateYear": "{year} generieren ({count} Monate)",
|
||||
"regenerateReport": "Neu generieren",
|
||||
"generatingMonthly": "Wird generiert...",
|
||||
"generatingYearly": "Wird generiert...",
|
||||
"reportInProgress": "{month} (in Bearbeitung)",
|
||||
"daysOfTotal": "{available} von {total} Tagen",
|
||||
"monthsOfTotal": "{available} von {total} Monaten",
|
||||
"monthlyAutoNote": "Der endgültige Bericht wird automatisch am 1. des nächsten Monats erstellt.",
|
||||
"yearlyAutoNote": "Der endgültige Bericht wird automatisch am 2. Januar erstellt.",
|
||||
"autoSendReports": "Berichte automatisch senden:",
|
||||
"autoSendSaved": "Automatische Versandeinstellungen gespeichert.",
|
||||
"autoSendSaveFailed": "Fehler beim Speichern der automatischen Versandeinstellungen.",
|
||||
"autoSendNoEmail": "E-Mail-Adresse im Reiter Information eingeben, um den automatischen Versand zu aktivieren",
|
||||
"thisMonthWeeklyReports": "Wöchentliche Berichte dieses Monats",
|
||||
"recentWeeklyReports": "Letzte Wochenberichte",
|
||||
"ai_analyzing": "KI analysiert...",
|
||||
|
|
@ -545,6 +544,7 @@
|
|||
"tourConfigurationContent": "Geräteeinstellungen für diese Installation anzeigen und ändern.",
|
||||
"tourHistoryTitle": "Verlauf",
|
||||
"tourHistoryContent": "Protokoll der Aktionen an dieser Installation — wer hat was und wann geändert.",
|
||||
"tourInstallationTicketsContent": "Support-Tickets für diese Installation anzeigen und verwalten — Probleme melden, Fortschritt verfolgen und KI-gestützte Diagnosen einsehen.",
|
||||
"tickets": "Tickets",
|
||||
"createTicket": "Ticket erstellen",
|
||||
"subject": "Betreff",
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@
|
|||
"inverterFirmwareVersion": "Inverter Firmware Version",
|
||||
"batteryFirmwareVersion": "Battery Firmware Version",
|
||||
"networkProvider": "Network Provider",
|
||||
"emailAddress": "Email Address",
|
||||
"customerName": "Customer name",
|
||||
"english": "English",
|
||||
"german": "German",
|
||||
|
|
@ -101,6 +102,7 @@
|
|||
"deleteFolder": "Delete Folder",
|
||||
"grantAccessToFolders": "Grant Access to Folders",
|
||||
"grantAccessToInstallations": "Grant Access to Installations",
|
||||
"searchInstallations": "Search installations...",
|
||||
"cannotloadloggingdata": "Cannot load logging data",
|
||||
"grantedAccessToUsers": "Granted access to users: ",
|
||||
"unableToGrantAccess": "Unable to grant access to: ",
|
||||
|
|
@ -113,7 +115,6 @@
|
|||
"reportTitle": "Weekly Performance Report",
|
||||
"weeklyInsights": "Weekly Insights",
|
||||
"missingDaysWarning": "Data available for {available}/{expected} days. Missing: {dates}",
|
||||
"weeklySavings": "Your Savings This Week",
|
||||
"solarEnergyUsed": "Energy Saved",
|
||||
"solarStayedHome": "solar + battery, not bought from grid",
|
||||
"daysOfYourUsage": "days of your usage",
|
||||
|
|
@ -121,10 +122,8 @@
|
|||
"atCHFRate": "at 0.39 CHF/kWh avg.",
|
||||
"solarCoverage": "Energy Independence",
|
||||
"fromSolarSub": "from your own solar + battery system",
|
||||
"avgDailyConsumption": "Avg Daily Consumption",
|
||||
"batteryEfficiency": "Battery Efficiency",
|
||||
"batteryEffSub": "discharge vs charge",
|
||||
"weeklySummary": "Weekly Summary",
|
||||
"metric": "Metric",
|
||||
"thisWeek": "This Week",
|
||||
"change": "Change",
|
||||
|
|
@ -134,11 +133,12 @@
|
|||
"gridExport": "Grid Export",
|
||||
"batteryInOut": "Battery Charge / Discharge",
|
||||
"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.",
|
||||
"refreshReport": "Refresh Report",
|
||||
"refreshing": "Refreshing...",
|
||||
"ok": "Ok",
|
||||
"grantedAccessToUser": "Granted access to user {name}",
|
||||
"proceed": "Proceed",
|
||||
|
|
@ -157,7 +157,7 @@
|
|||
"dailySummary": "Daily Summary",
|
||||
"noDataForDate": "No data available for the selected date.",
|
||||
"noHourlyData": "Hourly data not available for this day.",
|
||||
"currentWeekHint": "Current week (Mon–yesterday)",
|
||||
"currentWeekHint": "Last 7 days",
|
||||
"intradayChart": "Intraday Power Flow",
|
||||
"batteryPower": "Battery Power",
|
||||
"batterySoCLabel": "Battery SoC",
|
||||
|
|
@ -171,21 +171,20 @@
|
|||
"yearlyReportTitle": "Annual Performance Report",
|
||||
"monthlyInsights": "Monthly Insights",
|
||||
"yearlyInsights": "Annual Insights",
|
||||
"monthlySavings": "Your Savings This Month",
|
||||
"yearlySavings": "Your Savings This Year",
|
||||
"monthlySummary": "Monthly Summary",
|
||||
"yearlySummary": "Annual Summary",
|
||||
"total": "Total",
|
||||
"weeksAggregated": "{count} weeks aggregated",
|
||||
"monthsAggregated": "{count} months aggregated",
|
||||
"noMonthlyData": "No monthly reports available yet. Weekly reports will appear here for aggregation once generated.",
|
||||
"noYearlyData": "No yearly reports available yet. Monthly reports will appear here for aggregation once generated.",
|
||||
"availableForGeneration": "Available for Generation",
|
||||
"generateMonth": "Generate {month} {year} ({count} weeks)",
|
||||
"generateYear": "Generate {year} ({count} months)",
|
||||
"regenerateReport": "Regenerate",
|
||||
"generatingMonthly": "Generating...",
|
||||
"generatingYearly": "Generating...",
|
||||
"reportInProgress": "{month} (in progress)",
|
||||
"daysOfTotal": "{available} of {total} days",
|
||||
"monthsOfTotal": "{available} of {total} months",
|
||||
"monthlyAutoNote": "Final report will be automatically generated on the 1st of next month.",
|
||||
"yearlyAutoNote": "Final report will be automatically generated on January 2nd.",
|
||||
"autoSendReports": "Auto-send reports:",
|
||||
"autoSendSaved": "Auto-send preferences saved.",
|
||||
"autoSendSaveFailed": "Failed to save auto-send preferences.",
|
||||
"autoSendNoEmail": "Set email address in Information tab to enable auto-send",
|
||||
"thisMonthWeeklyReports": "This Month's Weekly Reports",
|
||||
"recentWeeklyReports": "Recent Weekly Reports",
|
||||
"ai_analyzing": "AI is analyzing...",
|
||||
|
|
@ -293,6 +292,7 @@
|
|||
"tourConfigurationContent": "View and modify device settings for this installation.",
|
||||
"tourHistoryTitle": "History",
|
||||
"tourHistoryContent": "Audit trail of actions performed on this installation — who changed what and when.",
|
||||
"tourInstallationTicketsContent": "View and manage support tickets for this installation — report issues, track progress, and see AI-powered diagnostics.",
|
||||
"tickets": "Tickets",
|
||||
"createTicket": "Create Ticket",
|
||||
"subject": "Subject",
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@
|
|||
"inverterFirmwareVersion": "Version firmware onduleur",
|
||||
"batteryFirmwareVersion": "Version firmware batterie",
|
||||
"networkProvider": "Gestionnaire de réseau",
|
||||
"emailAddress": "Adresse e-mail",
|
||||
"createNewFolder": "Nouveau dossier",
|
||||
"createNewUser": "Nouvel utilisateur",
|
||||
"customerName": "Nom du client",
|
||||
|
|
@ -113,6 +114,7 @@
|
|||
"deleteFolder": "Supprimer le dossier",
|
||||
"grantAccessToFolders": "Accorder l'accès aux dossiers",
|
||||
"grantAccessToInstallations": "Accorder l'accès aux installations",
|
||||
"searchInstallations": "Rechercher des installations...",
|
||||
"cannotloadloggingdata": "Impossible de charger les données de journalisation",
|
||||
"grantedAccessToUsers": "Accès accordé aux utilisateurs",
|
||||
"unableToGrantAccess": "Impossible d'accorder l'accès à",
|
||||
|
|
@ -125,7 +127,6 @@
|
|||
"reportTitle": "Rapport de performance hebdomadaire",
|
||||
"weeklyInsights": "Aperçus hebdomadaires",
|
||||
"missingDaysWarning": "Données disponibles pour {available}/{expected} jours. Manquants : {dates}",
|
||||
"weeklySavings": "Vos économies cette semaine",
|
||||
"solarEnergyUsed": "Énergie économisée",
|
||||
"solarStayedHome": "solaire + batterie, non achetée au réseau",
|
||||
"daysOfYourUsage": "jours de votre consommation",
|
||||
|
|
@ -133,10 +134,8 @@
|
|||
"atCHFRate": "à 0,39 CHF/kWh moy.",
|
||||
"solarCoverage": "Indépendance énergétique",
|
||||
"fromSolarSub": "de votre système solaire + batterie",
|
||||
"avgDailyConsumption": "Conso. quotidienne moy.",
|
||||
"batteryEfficiency": "Efficacité de la batterie",
|
||||
"batteryEffSub": "décharge vs charge",
|
||||
"weeklySummary": "Résumé hebdomadaire",
|
||||
"metric": "Métrique",
|
||||
"thisWeek": "Cette semaine",
|
||||
"change": "Variation",
|
||||
|
|
@ -146,11 +145,12 @@
|
|||
"gridExport": "Exportation réseau",
|
||||
"batteryInOut": "Batterie Charge / Décharge",
|
||||
"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.",
|
||||
"refreshReport": "Actualiser le rapport",
|
||||
"refreshing": "Actualisation...",
|
||||
"ok": "Ok",
|
||||
"grantedAccessToUser": "Accès accordé à l'utilisateur {name}",
|
||||
"proceed": "Continuer",
|
||||
|
|
@ -169,7 +169,7 @@
|
|||
"dailySummary": "Résumé du jour",
|
||||
"noDataForDate": "Aucune donnée disponible pour la date sélectionnée.",
|
||||
"noHourlyData": "Données horaires non disponibles pour ce jour.",
|
||||
"currentWeekHint": "Semaine en cours (lun–hier)",
|
||||
"currentWeekHint": "7 derniers jours",
|
||||
"intradayChart": "Flux d'énergie journalier",
|
||||
"batteryPower": "Puissance batterie",
|
||||
"batterySoCLabel": "SoC batterie",
|
||||
|
|
@ -183,21 +183,20 @@
|
|||
"yearlyReportTitle": "Rapport de performance annuel",
|
||||
"monthlyInsights": "Aperçus mensuels",
|
||||
"yearlyInsights": "Aperçus annuels",
|
||||
"monthlySavings": "Vos économies ce mois",
|
||||
"yearlySavings": "Vos économies cette année",
|
||||
"monthlySummary": "Résumé mensuel",
|
||||
"yearlySummary": "Résumé annuel",
|
||||
"total": "Total",
|
||||
"weeksAggregated": "{count} semaines agrégées",
|
||||
"monthsAggregated": "{count} mois agrégés",
|
||||
"noMonthlyData": "Aucun rapport mensuel disponible. Les rapports hebdomadaires apparaîtront ici pour l'agrégation.",
|
||||
"noYearlyData": "Aucun rapport annuel disponible. Les rapports mensuels apparaîtront ici pour l'agrégation.",
|
||||
"availableForGeneration": "Disponible pour génération",
|
||||
"generateMonth": "Générer {month} {year} ({count} semaines)",
|
||||
"generateYear": "Générer {year} ({count} mois)",
|
||||
"regenerateReport": "Régénérer",
|
||||
"generatingMonthly": "Génération en cours...",
|
||||
"generatingYearly": "Génération en cours...",
|
||||
"reportInProgress": "{month} (en cours)",
|
||||
"daysOfTotal": "{available} sur {total} jours",
|
||||
"monthsOfTotal": "{available} sur {total} mois",
|
||||
"monthlyAutoNote": "Le rapport final sera généré automatiquement le 1er du mois prochain.",
|
||||
"yearlyAutoNote": "Le rapport final sera généré automatiquement le 2 janvier.",
|
||||
"autoSendReports": "Envoi automatique des rapports :",
|
||||
"autoSendSaved": "Préférences d'envoi automatique enregistrées.",
|
||||
"autoSendSaveFailed": "Échec de l'enregistrement des préférences d'envoi automatique.",
|
||||
"autoSendNoEmail": "Définir l'adresse e-mail dans l'onglet Information pour activer l'envoi automatique",
|
||||
"thisMonthWeeklyReports": "Rapports hebdomadaires de ce mois",
|
||||
"recentWeeklyReports": "Derniers rapports hebdomadaires",
|
||||
"ai_analyzing": "L'IA analyse...",
|
||||
|
|
@ -545,6 +544,7 @@
|
|||
"tourConfigurationContent": "Afficher et modifier les paramètres de l'appareil pour cette installation.",
|
||||
"tourHistoryTitle": "Historique",
|
||||
"tourHistoryContent": "Journal des actions effectuées sur cette installation — qui a changé quoi et quand.",
|
||||
"tourInstallationTicketsContent": "Consultez et gérez les tickets de support pour cette installation — signalez des problèmes, suivez la progression et consultez les diagnostics IA.",
|
||||
"tickets": "Tickets",
|
||||
"createTicket": "Créer un ticket",
|
||||
"subject": "Objet",
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@
|
|||
"inverterFirmwareVersion": "Versione firmware inverter",
|
||||
"batteryFirmwareVersion": "Versione firmware batteria",
|
||||
"networkProvider": "Gestore di rete",
|
||||
"emailAddress": "Indirizzo e-mail",
|
||||
"customerName": "Nome cliente",
|
||||
"english": "Inglese",
|
||||
"german": "Tedesco",
|
||||
|
|
@ -101,6 +102,7 @@
|
|||
"deleteFolder": "Elimina cartella",
|
||||
"grantAccessToFolders": "Concedi accesso alle cartelle",
|
||||
"grantAccessToInstallations": "Concedi accesso alle installazioni",
|
||||
"searchInstallations": "Cerca installazioni...",
|
||||
"cannotloadloggingdata": "Impossibile caricare i dati di registro",
|
||||
"grantedAccessToUsers": "Accesso concesso agli utenti: ",
|
||||
"unableToGrantAccess": "Impossibile concedere l'accesso a: ",
|
||||
|
|
@ -136,7 +138,6 @@
|
|||
"reportTitle": "Rapporto settimanale sulle prestazioni",
|
||||
"weeklyInsights": "Approfondimenti settimanali",
|
||||
"missingDaysWarning": "Dati disponibili per {available}/{expected} giorni. Mancanti: {dates}",
|
||||
"weeklySavings": "I tuoi risparmi questa settimana",
|
||||
"solarEnergyUsed": "Energia risparmiata",
|
||||
"solarStayedHome": "solare + batteria, non acquistata dalla rete",
|
||||
"daysOfYourUsage": "giorni del tuo consumo",
|
||||
|
|
@ -144,10 +145,8 @@
|
|||
"atCHFRate": "a 0,39 CHF/kWh media",
|
||||
"solarCoverage": "Indipendenza energetica",
|
||||
"fromSolarSub": "dal proprio impianto solare + batteria",
|
||||
"avgDailyConsumption": "Consumo medio giornaliero",
|
||||
"batteryEfficiency": "Efficienza della batteria",
|
||||
"batteryEffSub": "scarica vs carica",
|
||||
"weeklySummary": "Riepilogo settimanale",
|
||||
"metric": "Metrica",
|
||||
"thisWeek": "Questa settimana",
|
||||
"change": "Variazione",
|
||||
|
|
@ -157,11 +156,12 @@
|
|||
"gridExport": "Esportazione rete",
|
||||
"batteryInOut": "Batteria Carica / Scarica",
|
||||
"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.",
|
||||
"refreshReport": "Aggiorna rapporto",
|
||||
"refreshing": "Aggiornamento...",
|
||||
"ok": "Ok",
|
||||
"grantedAccessToUser": "Accesso concesso all'utente {name}",
|
||||
"proceed": "Procedi",
|
||||
|
|
@ -180,7 +180,7 @@
|
|||
"dailySummary": "Riepilogo del giorno",
|
||||
"noDataForDate": "Nessun dato disponibile per la data selezionata.",
|
||||
"noHourlyData": "Dati orari non disponibili per questo giorno.",
|
||||
"currentWeekHint": "Settimana corrente (lun–ieri)",
|
||||
"currentWeekHint": "Ultimi 7 giorni",
|
||||
"intradayChart": "Flusso energetico giornaliero",
|
||||
"batteryPower": "Potenza batteria",
|
||||
"batterySoCLabel": "SoC batteria",
|
||||
|
|
@ -194,21 +194,20 @@
|
|||
"yearlyReportTitle": "Rapporto annuale sulle prestazioni",
|
||||
"monthlyInsights": "Approfondimenti mensili",
|
||||
"yearlyInsights": "Approfondimenti annuali",
|
||||
"monthlySavings": "I tuoi risparmi questo mese",
|
||||
"yearlySavings": "I tuoi risparmi quest'anno",
|
||||
"monthlySummary": "Riepilogo mensile",
|
||||
"yearlySummary": "Riepilogo annuale",
|
||||
"total": "Totale",
|
||||
"weeksAggregated": "{count} settimane aggregate",
|
||||
"monthsAggregated": "{count} mesi aggregati",
|
||||
"noMonthlyData": "Nessun rapporto mensile ancora disponibile. I rapporti settimanali appariranno qui per l'aggregazione.",
|
||||
"noYearlyData": "Nessun rapporto annuale ancora disponibile. I rapporti mensili appariranno qui per l'aggregazione.",
|
||||
"availableForGeneration": "Disponibile per la generazione",
|
||||
"generateMonth": "Genera {month} {year} ({count} settimane)",
|
||||
"generateYear": "Genera {year} ({count} mesi)",
|
||||
"regenerateReport": "Rigenera",
|
||||
"generatingMonthly": "Generazione in corso...",
|
||||
"generatingYearly": "Generazione in corso...",
|
||||
"reportInProgress": "{month} (in corso)",
|
||||
"daysOfTotal": "{available} di {total} giorni",
|
||||
"monthsOfTotal": "{available} di {total} mesi",
|
||||
"monthlyAutoNote": "Il rapporto finale verrà generato automaticamente il 1° del mese prossimo.",
|
||||
"yearlyAutoNote": "Il rapporto finale verrà generato automaticamente il 2 gennaio.",
|
||||
"autoSendReports": "Invio automatico rapporti:",
|
||||
"autoSendSaved": "Preferenze di invio automatico salvate.",
|
||||
"autoSendSaveFailed": "Impossibile salvare le preferenze di invio automatico.",
|
||||
"autoSendNoEmail": "Impostare l'indirizzo e-mail nella scheda Informazioni per attivare l'invio automatico",
|
||||
"thisMonthWeeklyReports": "Rapporti settimanali di questo mese",
|
||||
"recentWeeklyReports": "Ultimi rapporti settimanali",
|
||||
"ai_analyzing": "L'IA sta analizzando...",
|
||||
|
|
@ -545,6 +544,7 @@
|
|||
"tourConfigurationContent": "Visualizza e modifica le impostazioni del dispositivo per questa installazione.",
|
||||
"tourHistoryTitle": "Cronologia",
|
||||
"tourHistoryContent": "Registro delle azioni eseguite su questa installazione — chi ha cambiato cosa e quando.",
|
||||
"tourInstallationTicketsContent": "Visualizza e gestisci i ticket di supporto per questa installazione — segnala problemi, monitora i progressi e consulta le diagnosi IA.",
|
||||
"tickets": "Ticket",
|
||||
"createTicket": "Crea ticket",
|
||||
"subject": "Oggetto",
|
||||
|
|
|
|||
|
|
@ -1,17 +1,10 @@
|
|||
import { ReactNode, useContext, useEffect, useState } from 'react';
|
||||
import { ReactNode, useEffect, useState } from 'react';
|
||||
import { alpha, Box, lighten, useTheme } from '@mui/material';
|
||||
import { Outlet, useLocation } from 'react-router-dom';
|
||||
import Joyride, { CallBackProps, STATUS, Step } from 'react-joyride';
|
||||
import { useIntl, IntlShape } from 'react-intl';
|
||||
import Joyride, { CallBackProps, EVENTS, STATUS, Step } from 'react-joyride';
|
||||
import { useIntl } from 'react-intl';
|
||||
import { useTour } from 'src/contexts/TourContext';
|
||||
import { UserContext } from 'src/contexts/userContext';
|
||||
import { UserType } from 'src/interfaces/UserTypes';
|
||||
import {
|
||||
buildSodiohomeCustomerTourSteps, buildSodiohomePartnerTourSteps, buildSodiohomeAdminTourSteps,
|
||||
buildSalimaxCustomerTourSteps, buildSalimaxPartnerTourSteps, buildSalimaxAdminTourSteps,
|
||||
buildSodistoregridCustomerTourSteps, buildSodistoregridPartnerTourSteps, buildSodistoregridAdminTourSteps,
|
||||
buildSalidomoCustomerTourSteps, buildSalidomoPartnerTourSteps, buildSalidomoAdminTourSteps
|
||||
} from 'src/config/tourSteps';
|
||||
import { buildDynamicTourSteps } from 'src/config/tourSteps';
|
||||
|
||||
import Sidebar from './Sidebar';
|
||||
import Header from './Header';
|
||||
|
|
@ -22,38 +15,11 @@ interface SidebarLayoutProps {
|
|||
onSelectLanguage: (item: string) => void;
|
||||
}
|
||||
|
||||
function getTourSteps(pathname: string, userType: UserType, intl: IntlShape, isInsideInstallation: boolean): Step[] {
|
||||
const role = userType === UserType.admin ? 'admin'
|
||||
: userType === UserType.partner ? 'partner'
|
||||
: 'customer';
|
||||
|
||||
if (pathname.includes('/sodiohome_installations')) {
|
||||
if (role === 'admin') return buildSodiohomeAdminTourSteps(intl, isInsideInstallation);
|
||||
if (role === 'partner') return buildSodiohomePartnerTourSteps(intl, isInsideInstallation);
|
||||
return buildSodiohomeCustomerTourSteps(intl, isInsideInstallation);
|
||||
}
|
||||
if (pathname.includes('/salidomo_installations')) {
|
||||
if (role === 'admin') return buildSalidomoAdminTourSteps(intl, isInsideInstallation);
|
||||
if (role === 'partner') return buildSalidomoPartnerTourSteps(intl, isInsideInstallation);
|
||||
return buildSalidomoCustomerTourSteps(intl, isInsideInstallation);
|
||||
}
|
||||
if (pathname.includes('/sodistoregrid_installations')) {
|
||||
if (role === 'admin') return buildSodistoregridAdminTourSteps(intl, isInsideInstallation);
|
||||
if (role === 'partner') return buildSodistoregridPartnerTourSteps(intl, isInsideInstallation);
|
||||
return buildSodistoregridCustomerTourSteps(intl, isInsideInstallation);
|
||||
}
|
||||
// Salimax (/installations/) and Sodistore Max (/sodistore_installations/)
|
||||
if (role === 'admin') return buildSalimaxAdminTourSteps(intl, isInsideInstallation);
|
||||
if (role === 'partner') return buildSalimaxPartnerTourSteps(intl, isInsideInstallation);
|
||||
return buildSalimaxCustomerTourSteps(intl, isInsideInstallation);
|
||||
}
|
||||
|
||||
const SidebarLayout = (props: SidebarLayoutProps) => {
|
||||
const theme = useTheme();
|
||||
const intl = useIntl();
|
||||
const { runTour, stopTour } = useTour();
|
||||
const location = useLocation();
|
||||
const { currentUser } = useContext(UserContext);
|
||||
const [tourSteps, setTourSteps] = useState<Step[]>([]);
|
||||
const [tourReady, setTourReady] = useState(false);
|
||||
|
||||
|
|
@ -64,23 +30,22 @@ const SidebarLayout = (props: SidebarLayoutProps) => {
|
|||
}
|
||||
// Delay to let child components render their tour target elements
|
||||
const timer = setTimeout(() => {
|
||||
const userType = currentUser?.userType ?? UserType.client;
|
||||
const isInsideInstallation = location.pathname.includes('/installation/');
|
||||
const steps = getTourSteps(location.pathname, userType, intl, isInsideInstallation);
|
||||
const filtered = steps.filter((step) => {
|
||||
if (typeof step.target === 'string') {
|
||||
return document.querySelector(step.target) !== null;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
setTourSteps(filtered);
|
||||
const steps = buildDynamicTourSteps(intl, isInsideInstallation);
|
||||
setTourSteps(steps);
|
||||
setTourReady(true);
|
||||
}, 300);
|
||||
}, 500);
|
||||
return () => clearTimeout(timer);
|
||||
}, [runTour, location.pathname, currentUser?.userType, intl]);
|
||||
}, [runTour, location.pathname, intl]);
|
||||
|
||||
const handleJoyrideCallback = (data: CallBackProps) => {
|
||||
const { status } = data;
|
||||
const { status, step, type } = data;
|
||||
if (type === EVENTS.STEP_BEFORE && step?.target) {
|
||||
const el = document.querySelector(step.target as string);
|
||||
if (el) {
|
||||
el.scrollIntoView({ behavior: 'smooth', inline: 'center', block: 'nearest' });
|
||||
}
|
||||
}
|
||||
if (status === STATUS.FINISHED || status === STATUS.SKIPPED) {
|
||||
stopTour();
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue