Compare commits
7 Commits
e3248090da
...
ec0e8258b2
| Author | SHA1 | Date |
|---|---|---|
|
|
ec0e8258b2 | |
|
|
8c01912947 | |
|
|
67d5091093 | |
|
|
2cc8eebf37 | |
|
|
3a5c203664 | |
|
|
edb30286fa | |
|
|
88757c562b |
|
|
@ -498,6 +498,20 @@ public class Controller : ControllerBase
|
|||
.ToList();
|
||||
}
|
||||
|
||||
[HttpGet(nameof(GetAllUsers))]
|
||||
public ActionResult<IEnumerable<User>> GetAllUsers(Token authToken)
|
||||
{
|
||||
var user = Db.GetSession(authToken)?.User;
|
||||
if (user == null)
|
||||
return Unauthorized();
|
||||
if (user.UserType != 2) // admins only
|
||||
return Unauthorized();
|
||||
|
||||
return Db.Users
|
||||
.Select(u => u.HidePassword())
|
||||
.ToList();
|
||||
}
|
||||
|
||||
|
||||
[HttpGet(nameof(GetAllInstallationsFromProduct))]
|
||||
public ActionResult<IEnumerable<Installation>> GetAllInstallationsFromProduct(int product,Token authToken)
|
||||
|
|
@ -3047,6 +3061,45 @@ public class Controller : ControllerBase
|
|||
return Ok(summaries);
|
||||
}
|
||||
|
||||
// ── Battery full-charge low-voltage anomaly (temporary diagnostic) ──
|
||||
|
||||
[HttpGet(nameof(GetBatteryVoltageAnomalySummary))]
|
||||
public ActionResult<IEnumerable<Object>> GetBatteryVoltageAnomalySummary(Token authToken)
|
||||
{
|
||||
var user = Db.GetSession(authToken)?.User;
|
||||
if (user is null || user.UserType != 2) return Unauthorized();
|
||||
|
||||
// DetectedAt is "yyyy-MM-dd HH:mm:ss" — fixed-width, so lexicographic == chronological.
|
||||
// If the format ever changes, this ordering breaks silently.
|
||||
var summary = Db.BatteryVoltageAnomalies
|
||||
.ToList()
|
||||
.GroupBy(a => a.InstallationId)
|
||||
.Select(g =>
|
||||
{
|
||||
var latest = g.OrderByDescending(a => a.DetectedAt).First();
|
||||
return new
|
||||
{
|
||||
installationId = g.Key,
|
||||
lastDetectedAt = latest.DetectedAt,
|
||||
lastVoltage = latest.Voltage,
|
||||
lastBattery = latest.BatteryLabel,
|
||||
count = g.Count()
|
||||
};
|
||||
})
|
||||
.ToList();
|
||||
|
||||
return Ok(summary);
|
||||
}
|
||||
|
||||
[HttpGet(nameof(GetBatteryVoltageAnomalyLog))]
|
||||
public ActionResult<IEnumerable<BatteryVoltageAnomaly>> GetBatteryVoltageAnomalyLog(Int64 installationId, Token authToken)
|
||||
{
|
||||
var user = Db.GetSession(authToken)?.User;
|
||||
if (user is null || user.UserType != 2) return Unauthorized();
|
||||
|
||||
return Ok(Db.GetBatteryVoltageAnomalies(installationId));
|
||||
}
|
||||
|
||||
// ── On-Site Installer Checklist (sodistore home only) ──────────────
|
||||
|
||||
[HttpGet(nameof(GetOnSiteChecklistForInstallation))]
|
||||
|
|
|
|||
|
|
@ -0,0 +1,30 @@
|
|||
using SQLite;
|
||||
|
||||
namespace InnovEnergy.App.Backend.DataTypes;
|
||||
|
||||
// Temporary diagnostic: one row per battery cluster per hour where SOC=100% & pack voltage < 58V
|
||||
// ("BMS fake-full"). When this feature is no longer needed, drop the table, this type,
|
||||
// BatteryVoltageCheckService, BatteryVoltageAnomalyDetector, the two endpoints, the Db.cs
|
||||
// CreateTable entries (main + fileConnection), and the related Create/Read/Delete helpers.
|
||||
public class BatteryVoltageAnomaly
|
||||
{
|
||||
[PrimaryKey, AutoIncrement]
|
||||
public Int64 Id { get; set; }
|
||||
|
||||
[Indexed]
|
||||
public Int64 InstallationId { get; set; }
|
||||
|
||||
public String BatteryLabel { get; set; } = "";
|
||||
|
||||
public String DetectedAt { get; set; } = "";
|
||||
|
||||
[Indexed]
|
||||
public String Date { get; set; } = "";
|
||||
|
||||
public String DateHour { get; set; } = "";
|
||||
|
||||
public Double Voltage { get; set; }
|
||||
public Double Soc { get; set; }
|
||||
|
||||
public String CreatedAt { get; set; } = "";
|
||||
}
|
||||
|
|
@ -13,7 +13,8 @@ public static class ChecklistStepDefinitions
|
|||
new( 3, "Installation created on Monitor under correct product and folder",
|
||||
"""
|
||||
[
|
||||
{"text":"checklistStep3Sub1","checked":false}
|
||||
{"text":"checklistStep3Sub1","checked":false},
|
||||
{"text":"checklistStep3Sub2","checked":false}
|
||||
]
|
||||
"""),
|
||||
new( 4, "Gateway SD card configured, VPN and gateway name registered", NoSubtasks),
|
||||
|
|
@ -64,8 +65,5 @@ public static class ChecklistStepDefinitions
|
|||
new(11, "Software verified on site", NoSubtasks),
|
||||
new(12, "Installation online on Monitor", NoSubtasks),
|
||||
new(13, "Customer informed about Monitor account and reports", NoSubtasks),
|
||||
new(14, "User account created with correct folders and access", NoSubtasks),
|
||||
new(15, "Customer follow-up completed, feedback collected", NoSubtasks),
|
||||
new(16, "Further issues tracked via Ticket system", NoSubtasks),
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -368,11 +368,17 @@ public static class SessionMethods
|
|||
var sessionUser = session?.User;
|
||||
var originalUser = Db.GetUserById(editedUser?.Id);
|
||||
|
||||
return editedUser is not null
|
||||
&& sessionUser is not null
|
||||
&& originalUser is not null
|
||||
&& sessionUser.UserType !=0
|
||||
&& sessionUser.HasAccessTo(originalUser)
|
||||
if (editedUser is null || sessionUser is null || originalUser is null)
|
||||
return false;
|
||||
|
||||
// email must stay unique; pre-check to avoid hitting the DB [Unique] constraint (500)
|
||||
var emailOwner = Db.GetUserByEmail(editedUser.Email);
|
||||
|
||||
return sessionUser.UserType != 0
|
||||
&& originalUser.Id != 0 // belt: legacy root-id sentinel
|
||||
&& originalUser.ParentId > 0 // never edit the main/root admin (parentless top user; ParentId<=0, Id NOT necessarily 0)
|
||||
&& (sessionUser.UserType == 2 || sessionUser.HasAccessTo(originalUser)) // admins may edit any user
|
||||
&& (emailOwner is null || emailOwner.Id == editedUser.Id) // email not taken by another user
|
||||
&& editedUser
|
||||
.WithParentOf(originalUser) // prevent moving
|
||||
.WithPasswordOf(originalUser)
|
||||
|
|
@ -397,7 +403,10 @@ public static class SessionMethods
|
|||
return sessionUser is not null
|
||||
&& userToDelete is not null
|
||||
&& sessionUser.UserType !=0
|
||||
&& sessionUser.HasAccessTo(userToDelete)
|
||||
&& userToDelete.Id != 0 // belt: legacy root-id sentinel
|
||||
&& userToDelete.ParentId > 0 // never delete the main/root admin (the parentless top user; ParentId<=0, Id NOT necessarily 0)
|
||||
&& userToDelete.Id != sessionUser.Id // never self-delete (avoid lockout)
|
||||
&& (sessionUser.UserType == 2 || sessionUser.HasAccessTo(userToDelete)) // admins may delete any user
|
||||
&& Db.Delete(userToDelete);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -48,6 +48,11 @@ public static partial class Db
|
|||
return Insert(session);
|
||||
}
|
||||
|
||||
public static Boolean Create(BatteryVoltageAnomaly anomaly)
|
||||
{
|
||||
return Insert(anomaly);
|
||||
}
|
||||
|
||||
public static Boolean Create(InstallationAccess installationAccess)
|
||||
{
|
||||
return Insert(installationAccess);
|
||||
|
|
|
|||
|
|
@ -50,6 +50,9 @@ public static partial class Db
|
|||
public static TableQuery<OnSiteChecklistItem> OnSiteChecklistItems => Connection.Table<OnSiteChecklistItem>();
|
||||
public static TableQuery<OnSiteChecklistSignature> OnSiteChecklistSignatures => Connection.Table<OnSiteChecklistSignature>();
|
||||
|
||||
// Battery full-charge low-voltage anomaly log (temporary diagnostic)
|
||||
public static TableQuery<BatteryVoltageAnomaly> BatteryVoltageAnomalies => Connection.Table<BatteryVoltageAnomaly>();
|
||||
|
||||
|
||||
public static void Init()
|
||||
{
|
||||
|
|
@ -98,6 +101,9 @@ public static partial class Db
|
|||
// On-site installer checklist (sodistore home)
|
||||
Connection.CreateTable<OnSiteChecklistItem>();
|
||||
Connection.CreateTable<OnSiteChecklistSignature>();
|
||||
|
||||
// Battery full-charge low-voltage anomaly log (temporary diagnostic)
|
||||
Connection.CreateTable<BatteryVoltageAnomaly>();
|
||||
});
|
||||
|
||||
// One-time migration: normalize legacy long-form language values to ISO codes
|
||||
|
|
@ -168,6 +174,14 @@ public static partial class Db
|
|||
// raw-string JSON form preserving each row's existing checked state.
|
||||
BackfillStep10Sub3();
|
||||
|
||||
// One-time backfill: the former step 14 (user account created) became a subtask of step 3.
|
||||
// Append it to existing step-3 rows, preserving each row's existing checked state.
|
||||
BackfillStep3Sub2();
|
||||
|
||||
// One-time cleanup: steps 14 (now a subtask of step 3), 15 and 16 were removed from the
|
||||
// onboarding checklist. Drop any rows seeded before removal.
|
||||
Connection.Execute("DELETE FROM ChecklistItem WHERE StepNumber IN (14, 15, 16)");
|
||||
|
||||
// One-time cleanup: step 9 (Abschluss) was removed from the on-site checklist —
|
||||
// the e-signature block now handles sign-off. Drop any rows seeded before removal.
|
||||
Connection.Execute("DELETE FROM OnSiteChecklistItem WHERE StepNumber = 9");
|
||||
|
|
@ -200,6 +214,31 @@ public static partial class Db
|
|||
}
|
||||
}
|
||||
|
||||
// Must run AFTER the step-3 Sub1 SQL backfill above: that step populates rows whose Subtasks
|
||||
// were NULL/empty, after which they become eligible for the Sub2 append below.
|
||||
private static void BackfillStep3Sub2()
|
||||
{
|
||||
var rows = Connection.Query<ChecklistItem>(
|
||||
"SELECT * FROM ChecklistItem WHERE StepNumber = 3 AND Subtasks IS NOT NULL AND Subtasks != ''");
|
||||
foreach (var row in rows)
|
||||
{
|
||||
if (row.Subtasks is null) continue;
|
||||
if (row.Subtasks.Contains("checklistStep3Sub2")) continue;
|
||||
|
||||
// Insert sub2 before the closing bracket. Works regardless of indentation/whitespace
|
||||
// since we only look for the last `]`.
|
||||
var lastBracket = row.Subtasks.LastIndexOf(']');
|
||||
if (lastBracket < 0) continue;
|
||||
|
||||
var head = row.Subtasks.Substring(0, lastBracket).TrimEnd();
|
||||
// head ends with `}` of the last existing subtask.
|
||||
var newJson = head + ",{\"text\":\"checklistStep3Sub2\",\"checked\":false}]";
|
||||
Connection.Execute(
|
||||
"UPDATE ChecklistItem SET Subtasks = ? WHERE Id = ?",
|
||||
newJson, row.Id);
|
||||
}
|
||||
}
|
||||
|
||||
private static SQLiteConnection InitConnection()
|
||||
{
|
||||
var latestDb = new DirectoryInfo("DbBackups")
|
||||
|
|
@ -248,6 +287,9 @@ public static partial class Db
|
|||
fileConnection.CreateTable<OnSiteChecklistItem>();
|
||||
fileConnection.CreateTable<OnSiteChecklistSignature>();
|
||||
|
||||
// Battery full-charge low-voltage anomaly log (temporary diagnostic)
|
||||
fileConnection.CreateTable<BatteryVoltageAnomaly>();
|
||||
|
||||
// Migrate new columns: set defaults for existing rows where NULL or empty
|
||||
fileConnection.Execute("UPDATE Installation SET ExternalEms = 'No' WHERE ExternalEms IS NULL OR ExternalEms = ''");
|
||||
fileConnection.Execute("UPDATE Installation SET InstallationModel = '' WHERE InstallationModel IS NULL");
|
||||
|
|
|
|||
|
|
@ -164,8 +164,18 @@ public static partial class Db
|
|||
|
||||
Boolean DeleteUserAndHisDependencies()
|
||||
{
|
||||
// Re-parent the deleted user's children up to its own parent so no subtree is orphaned
|
||||
// (a dangling ParentId would make the children invisible/unmanageable in the tree).
|
||||
var children = Users.Where(u => u.ParentId == user.Id).ToList();
|
||||
foreach (var child in children)
|
||||
{
|
||||
child.ParentId = user.ParentId;
|
||||
Connection.Update(child);
|
||||
}
|
||||
|
||||
FolderAccess .Delete(u => u.UserId == user.Id);
|
||||
InstallationAccess.Delete(u => u.UserId == user.Id);
|
||||
Sessions .Delete(s => s.UserId == user.Id); // kill the deleted user's login sessions immediately
|
||||
return Users.Delete(u => u.Id == user.Id) > 0;
|
||||
}
|
||||
}
|
||||
|
|
@ -365,4 +375,9 @@ public static partial class Db
|
|||
Backup();
|
||||
Console.WriteLine($"[Db] Cleanup: {oldDailyIds.Count} daily, {oldHourlyIds.Count} hourly, {oldWeeklyIds.Count} weekly, {oldMonthlyIds.Count} monthly, {oldCacheIds.Count} insight cache records deleted (cutoff {cutoff}).");
|
||||
}
|
||||
|
||||
public static void DeleteBatteryVoltageAnomaliesBefore(String isoDate)
|
||||
{
|
||||
Connection.Execute("DELETE FROM BatteryVoltageAnomaly WHERE Date < ?", isoDate);
|
||||
}
|
||||
}
|
||||
|
|
@ -272,4 +272,19 @@ public static partial class Db
|
|||
|
||||
public static OnSiteChecklistSignature? GetOnSiteChecklistSignature(Int64 installationId)
|
||||
=> OnSiteChecklistSignatures.FirstOrDefault(s => s.InstallationId == installationId);
|
||||
|
||||
// ── BatteryVoltageAnomaly Queries (temporary diagnostic) ────────────
|
||||
|
||||
public static Boolean BatteryVoltageAnomalyExists(Int64 installationId, String batteryLabel, String dateHour)
|
||||
=> BatteryVoltageAnomalies.Any(a =>
|
||||
a.InstallationId == installationId &&
|
||||
a.BatteryLabel == batteryLabel &&
|
||||
a.DateHour == dateHour);
|
||||
|
||||
public static List<BatteryVoltageAnomaly> GetBatteryVoltageAnomalies(Int64 installationId)
|
||||
=> BatteryVoltageAnomalies
|
||||
.Where(a => a.InstallationId == installationId)
|
||||
.ToList()
|
||||
.OrderByDescending(a => a.DetectedAt)
|
||||
.ToList();
|
||||
}
|
||||
|
|
@ -36,6 +36,7 @@ public static class Program
|
|||
AlarmReviewService.StartDailyScheduler();
|
||||
DailyIngestionService.StartScheduler();
|
||||
ReportAggregationService.StartScheduler();
|
||||
BatteryVoltageCheckService.StartScheduler();
|
||||
var builder = WebApplication.CreateBuilder(args);
|
||||
|
||||
RabbitMqManager.InitializeEnvironment();
|
||||
|
|
|
|||
|
|
@ -0,0 +1,113 @@
|
|||
using System.Text;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace InnovEnergy.App.Backend.Services;
|
||||
|
||||
public readonly record struct VoltageAnomaly(String BatteryLabel, Double Soc, Double Voltage, DateTime? Timestamp);
|
||||
|
||||
public static class BatteryVoltageAnomalyDetector
|
||||
{
|
||||
private const Double FullSoc = 100.0;
|
||||
private const Double LowVoltageVolt = 58.0;
|
||||
private const Int32 MaxClusters = 8;
|
||||
|
||||
public static List<VoltageAnomaly> Detect(String liveJson, Int32 device)
|
||||
{
|
||||
var decoded = Decode(liveJson);
|
||||
using var doc = JsonDocument.Parse(decoded);
|
||||
var root = Unwrap(doc.RootElement);
|
||||
var ts = ReadTimestamp(root);
|
||||
|
||||
return device switch
|
||||
{
|
||||
3 => DetectGrowatt(root, ts),
|
||||
4 => DetectSinexcel(root, ts),
|
||||
_ => new List<VoltageAnomaly>(),
|
||||
};
|
||||
}
|
||||
|
||||
private static List<VoltageAnomaly> DetectSinexcel(JsonElement root, DateTime? ts)
|
||||
{
|
||||
var found = new List<VoltageAnomaly>();
|
||||
if (!root.TryGetProperty("Devices", out var devices) || devices.ValueKind != JsonValueKind.Object)
|
||||
{
|
||||
Console.WriteLine("[BattVoltage] Sinexcel JSON has no Devices node — check live-file shape assumption.");
|
||||
return found;
|
||||
}
|
||||
|
||||
foreach (var dev in devices.EnumerateObject())
|
||||
{
|
||||
for (var cl = 1; cl <= MaxClusters; cl++)
|
||||
{
|
||||
// Battery1 uses "SocSecondvalue" (capital S), Battery2 uses "Socsecondvalue"
|
||||
// (lowercase s) per the documented live-data schema; try both casings.
|
||||
var soc = ReadDouble(dev.Value, $"Battery{cl}Soc")
|
||||
?? ReadDouble(dev.Value, $"Battery{cl}SocSecondvalue")
|
||||
?? ReadDouble(dev.Value, $"Battery{cl}Socsecondvalue");
|
||||
var v = ReadDouble(dev.Value, $"Battery{cl}PackTotalVoltage");
|
||||
if (soc is null || v is null) continue;
|
||||
if (IsAnomaly(soc.Value, v.Value))
|
||||
found.Add(new VoltageAnomaly($"Inv{dev.Name}/Bat{cl}", soc.Value, v.Value, ts));
|
||||
}
|
||||
}
|
||||
return found;
|
||||
}
|
||||
|
||||
private static List<VoltageAnomaly> DetectGrowatt(JsonElement root, DateTime? ts)
|
||||
{
|
||||
var found = new List<VoltageAnomaly>();
|
||||
for (var i = 1; i <= MaxClusters; i++)
|
||||
{
|
||||
var soc = ReadDouble(root, $"Battery{i}Soc");
|
||||
var v = ReadDouble(root, $"Battery{i}Voltage");
|
||||
if (soc is null || v is null) continue;
|
||||
if (IsAnomaly(soc.Value, v.Value))
|
||||
found.Add(new VoltageAnomaly($"Bat{i}", soc.Value, v.Value, ts));
|
||||
}
|
||||
return found;
|
||||
}
|
||||
|
||||
private static Boolean IsAnomaly(Double soc, Double v) => soc >= FullSoc && v < LowVoltageVolt;
|
||||
|
||||
private static Double? ReadDouble(JsonElement e, String name)
|
||||
{
|
||||
if (!e.TryGetProperty(name, out var p)) return null;
|
||||
if (p.ValueKind == JsonValueKind.Number && p.TryGetDouble(out var d)) return d;
|
||||
if (p.ValueKind == JsonValueKind.String &&
|
||||
Double.TryParse(p.GetString(), System.Globalization.NumberStyles.Float,
|
||||
System.Globalization.CultureInfo.InvariantCulture, out var ds)) return ds;
|
||||
return null;
|
||||
}
|
||||
|
||||
private static DateTime? ReadTimestamp(JsonElement e)
|
||||
{
|
||||
if (!e.TryGetProperty("Timestamp", out var p)) return null;
|
||||
if (p.ValueKind == JsonValueKind.String && p.TryGetDateTime(out var dt))
|
||||
{
|
||||
// Unspecified-Kind (no offset in source) is assumed UTC; otherwise convert.
|
||||
// Without this, downstream ToString("yyyy-MM-dd HH") prints local-time for
|
||||
// some sources and UTC for others, breaking the (..., DateHour) dedup key
|
||||
// and the cleanup-cutoff comparison.
|
||||
return dt.Kind == DateTimeKind.Unspecified
|
||||
? DateTime.SpecifyKind(dt, DateTimeKind.Utc)
|
||||
: dt.ToUniversalTime();
|
||||
}
|
||||
if (p.ValueKind == JsonValueKind.Number && p.TryGetInt64(out var unix))
|
||||
return DateTimeOffset.FromUnixTimeSeconds(unix).UtcDateTime;
|
||||
return null;
|
||||
}
|
||||
|
||||
private static JsonElement Unwrap(JsonElement root)
|
||||
=> root.ValueKind == JsonValueKind.Object &&
|
||||
root.TryGetProperty("InverterRecord", out var inner)
|
||||
? inner
|
||||
: root;
|
||||
|
||||
private static String Decode(String raw)
|
||||
{
|
||||
var t = raw.Trim();
|
||||
if (t.StartsWith('{') || t.StartsWith('[')) return raw;
|
||||
try { return Encoding.UTF8.GetString(Convert.FromBase64String(t)); }
|
||||
catch { return raw; }
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,144 @@
|
|||
using InnovEnergy.App.Backend.Database;
|
||||
using InnovEnergy.App.Backend.DataTypes;
|
||||
using InnovEnergy.App.Backend.DataTypes.Methods;
|
||||
using InnovEnergy.Lib.S3Utils;
|
||||
using InnovEnergy.Lib.S3Utils.DataTypes;
|
||||
using S3Region = InnovEnergy.Lib.S3Utils.DataTypes.S3Region;
|
||||
|
||||
namespace InnovEnergy.App.Backend.Services;
|
||||
|
||||
public static class BatteryVoltageCheckService
|
||||
{
|
||||
private static Timer? _timer;
|
||||
private const Int32 WindowStartUtc = 8;
|
||||
private const Int32 WindowEndUtc = 17;
|
||||
private const Int32 RetentionDays = 90;
|
||||
private const Int64 MinUnixTs = 1_000_000_000L;
|
||||
|
||||
public static void StartScheduler()
|
||||
{
|
||||
_timer = new Timer(_ =>
|
||||
{
|
||||
try { RunOnce().GetAwaiter().GetResult(); }
|
||||
catch (Exception ex) { Console.Error.WriteLine($"[BattVoltage] Scheduler error: {ex.Message}"); }
|
||||
}, null, TimeSpan.FromMinutes(1), TimeSpan.FromMinutes(30));
|
||||
|
||||
Console.WriteLine($"[BattVoltage] Scheduler started (every 30 min, UTC {WindowStartUtc:00}:00–{WindowEndUtc:00}:00).");
|
||||
}
|
||||
|
||||
private static async Task RunOnce()
|
||||
{
|
||||
// Retention runs regardless of the midday window — otherwise a backend whose
|
||||
// uptime never overlaps 08–17 UTC (frequent evening restarts) never prunes.
|
||||
CleanupOld();
|
||||
|
||||
var hour = DateTime.UtcNow.Hour;
|
||||
if (hour < WindowStartUtc || hour >= WindowEndUtc) return;
|
||||
|
||||
var installations = Db.Installations
|
||||
.Where(i => (i.Product == (Int32)ProductType.SodioHome ||
|
||||
i.Product == (Int32)ProductType.SodistorePro)
|
||||
&& i.DataCollectionEnabled)
|
||||
.ToList();
|
||||
|
||||
var total = 0;
|
||||
foreach (var inst in installations)
|
||||
{
|
||||
try { total += await CheckInstallation(inst); }
|
||||
catch (Exception ex) { Console.Error.WriteLine($"[BattVoltage] Inst {inst.Id} failed: {ex.Message}"); }
|
||||
}
|
||||
|
||||
if (total > 0) Console.WriteLine($"[BattVoltage] Run complete: {total} new anomaly row(s).");
|
||||
}
|
||||
|
||||
private static async Task<Int32> CheckInstallation(Installation inst)
|
||||
{
|
||||
var liveJson = await TryReadNewestLiveFile(inst);
|
||||
if (liveJson is null) return 0;
|
||||
|
||||
List<VoltageAnomaly> anomalies;
|
||||
try { anomalies = BatteryVoltageAnomalyDetector.Detect(liveJson, inst.Device); }
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.Error.WriteLine($"[BattVoltage] Inst {inst.Id} detect failed: {ex.Message}");
|
||||
return 0;
|
||||
}
|
||||
|
||||
var inserted = 0;
|
||||
foreach (var a in anomalies)
|
||||
{
|
||||
var detected = a.Timestamp ?? DateTime.UtcNow;
|
||||
var dateHour = detected.ToString("yyyy-MM-dd HH");
|
||||
if (Db.BatteryVoltageAnomalyExists(inst.Id, a.BatteryLabel, dateHour)) continue;
|
||||
|
||||
Db.Create(new BatteryVoltageAnomaly
|
||||
{
|
||||
InstallationId = inst.Id,
|
||||
BatteryLabel = a.BatteryLabel,
|
||||
DetectedAt = detected.ToString("yyyy-MM-dd HH:mm:ss"),
|
||||
Date = detected.ToString("yyyy-MM-dd"),
|
||||
DateHour = dateHour,
|
||||
Voltage = Math.Round(a.Voltage, 2),
|
||||
Soc = a.Soc,
|
||||
CreatedAt = DateTime.UtcNow.ToString("o"),
|
||||
});
|
||||
inserted++;
|
||||
Console.WriteLine($"[BattVoltage] Inst {inst.Id} {a.BatteryLabel}: SOC {a.Soc} @ {a.Voltage:F2}V");
|
||||
}
|
||||
return inserted;
|
||||
}
|
||||
|
||||
private static async Task<String?> TryReadNewestLiveFile(Installation inst)
|
||||
{
|
||||
try
|
||||
{
|
||||
var region = new S3Region($"https://{inst.S3Region}.{inst.S3Provider}", ExoCmd.S3Credentials!);
|
||||
var bucket = region.Bucket(inst.BucketName());
|
||||
|
||||
// Prefix-scope to recent files: first 5 digits of current unix-ts covers
|
||||
// a ~27h window (vs a full-bucket scan of potentially 100k+ objects on
|
||||
// 10-second cadence). At a digit boundary we may briefly miss until next
|
||||
// poll — acceptable for a 30-min temporary diagnostic.
|
||||
var nowTs = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
|
||||
var prefix = (nowTs / 100_000L).ToString();
|
||||
|
||||
S3Url? newest = null;
|
||||
var newestTs = Int64.MinValue;
|
||||
|
||||
await foreach (var url in bucket.ListObjects(prefix))
|
||||
{
|
||||
if (!TryParseUnixTs(url.Path, out var ts)) continue;
|
||||
if (ts <= newestTs) continue;
|
||||
newestTs = ts;
|
||||
newest = url;
|
||||
}
|
||||
|
||||
if (newest is null) return null;
|
||||
return await newest.GetObjectAsString();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.Error.WriteLine($"[BattVoltage] Inst {inst.Id} S3 read failed: {ex.Message}");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private static Boolean TryParseUnixTs(String path, out Int64 ts)
|
||||
{
|
||||
ts = 0;
|
||||
var slash = path.LastIndexOf('/');
|
||||
var name = slash >= 0 ? path[(slash + 1)..] : path;
|
||||
var dot = name.LastIndexOf('.');
|
||||
var stem = dot > 0 ? name[..dot] : name;
|
||||
if (!Int64.TryParse(stem, out var v)) return false;
|
||||
if (v < MinUnixTs) return false;
|
||||
ts = v;
|
||||
return true;
|
||||
}
|
||||
|
||||
private static void CleanupOld()
|
||||
{
|
||||
var cutoff = DateTime.UtcNow.AddDays(-RetentionDays).ToString("yyyy-MM-dd");
|
||||
Db.DeleteBatteryVoltageAnomaliesBefore(cutoff);
|
||||
}
|
||||
}
|
||||
|
|
@ -188,21 +188,22 @@ public static class RabbitMqManager
|
|||
|
||||
Db.UpdateInstallationStatus(installationId, receivedStatusMessage.Status);
|
||||
|
||||
const int AlarmStatus = 2;
|
||||
var isSodistore = installation.Product is 2 or 3 or 4 or 5;
|
||||
if (isSodistore
|
||||
&& prevStatus != AlarmStatus
|
||||
&& receivedStatusMessage.Status == AlarmStatus)
|
||||
{
|
||||
var prev = prevStatus;
|
||||
var alarmsSnapshot = (IReadOnlyList<AlarmOrWarning>)
|
||||
(receivedStatusMessage.Alarms?.ToList() ?? new List<AlarmOrWarning>());
|
||||
_ = Task.Run(async () =>
|
||||
{
|
||||
try { await AutoTicketService.MaybeCreateForAlarmAsync(installation, prev, alarmsSnapshot); }
|
||||
catch (Exception ex) { Console.WriteLine($"[AutoTicket] alarm failed for {installationId}: {ex.Message}"); }
|
||||
});
|
||||
}
|
||||
// Automatic ticket creation on error/warning (alarm) state — disabled.
|
||||
// const int AlarmStatus = 2;
|
||||
// var isSodistore = installation.Product is 2 or 3 or 4 or 5;
|
||||
// if (isSodistore
|
||||
// && prevStatus != AlarmStatus
|
||||
// && receivedStatusMessage.Status == AlarmStatus)
|
||||
// {
|
||||
// var prev = prevStatus;
|
||||
// var alarmsSnapshot = (IReadOnlyList<AlarmOrWarning>)
|
||||
// (receivedStatusMessage.Alarms?.ToList() ?? new List<AlarmOrWarning>());
|
||||
// _ = Task.Run(async () =>
|
||||
// {
|
||||
// try { await AutoTicketService.MaybeCreateForAlarmAsync(installation, prev, alarmsSnapshot); }
|
||||
// catch (Exception ex) { Console.WriteLine($"[AutoTicket] alarm failed for {installationId}: {ex.Message}"); }
|
||||
// });
|
||||
// }
|
||||
|
||||
//Console.WriteLine("----------------------------------------------");
|
||||
//If the status has changed, update all the connected front-ends regarding this installation
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import React, { useEffect, useMemo, useState } from 'react';
|
||||
import React, { useContext, useEffect, useMemo, useState } from 'react';
|
||||
import {
|
||||
Card,
|
||||
Container,
|
||||
Grid,
|
||||
Paper,
|
||||
|
|
@ -24,6 +25,10 @@ import {
|
|||
ActiveCluster,
|
||||
getActiveClusters
|
||||
} from '../Information/installationSetupUtils';
|
||||
import { UserContext } from 'src/contexts/userContext';
|
||||
import { UserType } from 'src/interfaces/UserTypes';
|
||||
import axiosConfig from 'src/Resources/axiosConfig';
|
||||
import { BatteryAnomalyLogEntry } from 'src/interfaces/BatteryAnomalyTypes';
|
||||
|
||||
interface BatteryViewSodioHomeProps {
|
||||
values: JSONRecordData;
|
||||
|
|
@ -47,6 +52,22 @@ function BatteryViewSodioHome(props: BatteryViewSodioHomeProps) {
|
|||
|
||||
const hasDevices = !!inverter?.Devices;
|
||||
|
||||
const { currentUser } = useContext(UserContext);
|
||||
const showAnomalyLog = currentUser?.userType === UserType.admin;
|
||||
const [anomalyLog, setAnomalyLog] = useState<BatteryAnomalyLogEntry[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!showAnomalyLog || !props.installationId) return;
|
||||
axiosConfig
|
||||
.get('/GetBatteryVoltageAnomalyLog', {
|
||||
params: { installationId: props.installationId }
|
||||
})
|
||||
.then((res) => {
|
||||
if (Array.isArray(res.data)) setAnomalyLog(res.data);
|
||||
})
|
||||
.catch(() => setAnomalyLog([]));
|
||||
}, [showAnomalyLog, props.installationId]);
|
||||
|
||||
const activeClusters: ActiveCluster[] = useMemo(() => {
|
||||
const parsed = getActiveClusters(batterySerialNumbers || '');
|
||||
if (parsed.length > 0) return parsed;
|
||||
|
|
@ -376,6 +397,67 @@ function BatteryViewSodioHome(props: BatteryViewSodioHomeProps) {
|
|||
</TableContainer>
|
||||
</Container>
|
||||
)}
|
||||
{showAnomalyLog && (
|
||||
<Container maxWidth="lg" sx={{ marginTop: '20px' }}>
|
||||
<Card>
|
||||
<Typography
|
||||
variant="h6"
|
||||
sx={{ padding: '12px 16px', fontWeight: 'bold' }}
|
||||
>
|
||||
<FormattedMessage
|
||||
id="voltageAnomalyLogTitle"
|
||||
defaultMessage="Full-charge low-voltage log"
|
||||
/>
|
||||
</Typography>
|
||||
<TableContainer>
|
||||
<Table size="small">
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell>
|
||||
<FormattedMessage id="battery" defaultMessage="Battery" />
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<FormattedMessage
|
||||
id="voltageAnomalyTime"
|
||||
defaultMessage="Time"
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<FormattedMessage id="voltage" defaultMessage="Voltage" />
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<FormattedMessage id="soc" defaultMessage="SOC" />
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{anomalyLog.length === 0 ? (
|
||||
<TableRow>
|
||||
<TableCell colSpan={4} sx={{ textAlign: 'center' }}>
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
<FormattedMessage
|
||||
id="noVoltageAnomalies"
|
||||
defaultMessage="No anomalies recorded"
|
||||
/>
|
||||
</Typography>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
) : (
|
||||
anomalyLog.map((entry) => (
|
||||
<TableRow key={entry.id}>
|
||||
<TableCell>{entry.batteryLabel}</TableCell>
|
||||
<TableCell>{entry.detectedAt}</TableCell>
|
||||
<TableCell>{entry.voltage.toFixed(2)} V</TableCell>
|
||||
<TableCell>{entry.soc.toFixed(0)} %</TableCell>
|
||||
</TableRow>
|
||||
))
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
</Card>
|
||||
</Container>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ function phaseId(done: number, total: number): string {
|
|||
if (total === 0) return 'checklistPhaseEmpty';
|
||||
if (done >= total) return 'checklistPhaseComplete';
|
||||
if (done <= 5) return 'checklistPhasePreparation';
|
||||
if (done <= 12) return 'checklistPhaseOnSite';
|
||||
if (done <= 11) return 'checklistPhaseOnSite';
|
||||
return 'checklistPhaseHandover';
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -550,7 +550,7 @@ const FlatInstallationView = (props: FlatInstallationViewProps) => {
|
|||
return (
|
||||
<SetupProgress
|
||||
done={summary?.done ?? 0}
|
||||
total={summary?.total ?? 16}
|
||||
total={summary?.total ?? 13}
|
||||
/>
|
||||
);
|
||||
})()}
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ import { useLocation, useNavigate } from 'react-router-dom';
|
|||
import routes from '../../../Resources/routes.json';
|
||||
import CancelIcon from '@mui/icons-material/Cancel';
|
||||
import BuildIcon from '@mui/icons-material/Build';
|
||||
import WarningAmberIcon from '@mui/icons-material/WarningAmber';
|
||||
import { getDeviceTypeName } from '../Information/installationSetupUtils';
|
||||
import { UserContext } from 'src/contexts/userContext';
|
||||
import { UserType } from 'src/interfaces/UserTypes';
|
||||
|
|
@ -27,6 +28,10 @@ import {
|
|||
CHECKLIST_ENABLED_PRODUCTS,
|
||||
ChecklistSummary
|
||||
} from 'src/interfaces/ChecklistTypes';
|
||||
import {
|
||||
BatteryAnomalySummary,
|
||||
VOLTAGE_ANOMALY_PRODUCTS
|
||||
} from 'src/interfaces/BatteryAnomalyTypes';
|
||||
import SetupProgress from '../Checklist/SetupProgress';
|
||||
|
||||
interface FlatInstallationViewProps {
|
||||
|
|
@ -44,12 +49,18 @@ const FlatInstallationView = (props: FlatInstallationViewProps) => {
|
|||
const showChecklistColumn =
|
||||
currentUser?.userType === UserType.admin &&
|
||||
CHECKLIST_ENABLED_PRODUCTS.has(props.product ?? -1);
|
||||
const showAnomalyColumn =
|
||||
currentUser?.userType === UserType.admin &&
|
||||
VOLTAGE_ANOMALY_PRODUCTS.has(props.product ?? -1);
|
||||
const isListViewPath =
|
||||
currentLocation.pathname === baseRoute + 'list' ||
|
||||
currentLocation.pathname === baseRoute + routes.list;
|
||||
const [progressMap, setProgressMap] = useState<Map<number, ChecklistSummary>>(
|
||||
new Map()
|
||||
);
|
||||
const [anomalyMap, setAnomalyMap] = useState<Map<number, BatteryAnomalySummary>>(
|
||||
new Map()
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (!showChecklistColumn || !isListViewPath) return;
|
||||
|
|
@ -66,6 +77,21 @@ const FlatInstallationView = (props: FlatInstallationViewProps) => {
|
|||
.catch(() => setProgressMap(new Map()));
|
||||
}, [showChecklistColumn, isListViewPath]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!showAnomalyColumn || !isListViewPath) return;
|
||||
axiosConfig
|
||||
.get('/GetBatteryVoltageAnomalySummary')
|
||||
.then((res) => {
|
||||
if (!Array.isArray(res.data)) return;
|
||||
const map = new Map<number, BatteryAnomalySummary>();
|
||||
res.data.forEach((s: BatteryAnomalySummary) => {
|
||||
map.set(s.installationId, s);
|
||||
});
|
||||
setAnomalyMap(map);
|
||||
})
|
||||
.catch(() => setAnomalyMap(new Map()));
|
||||
}, [showAnomalyColumn, isListViewPath]);
|
||||
|
||||
const sortedInstallations = useMemo(() => {
|
||||
return [...props.installations].sort((a, b) => {
|
||||
// Data-collection-disabled installations sink below everything (even offline).
|
||||
|
|
@ -158,6 +184,14 @@ const FlatInstallationView = (props: FlatInstallationViewProps) => {
|
|||
/>
|
||||
</TableCell>
|
||||
)}
|
||||
{showAnomalyColumn && (
|
||||
<TableCell>
|
||||
<FormattedMessage
|
||||
id="batteryVoltageAnomaly"
|
||||
defaultMessage="Full @ low voltage"
|
||||
/>
|
||||
</TableCell>
|
||||
)}
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
|
|
@ -329,12 +363,25 @@ const FlatInstallationView = (props: FlatInstallationViewProps) => {
|
|||
return (
|
||||
<SetupProgress
|
||||
done={summary?.done ?? 0}
|
||||
total={summary?.total ?? 16}
|
||||
total={summary?.total ?? 13}
|
||||
/>
|
||||
);
|
||||
})()}
|
||||
</TableCell>
|
||||
)}
|
||||
{showAnomalyColumn && (
|
||||
<TableCell>
|
||||
{anomalyMap.has(installation.id) && (
|
||||
<WarningAmberIcon
|
||||
style={{
|
||||
width: '20px',
|
||||
height: '20px',
|
||||
color: '#f7b34d'
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</TableCell>
|
||||
)}
|
||||
</HoverableTableRow>
|
||||
);
|
||||
})}
|
||||
|
|
|
|||
|
|
@ -48,12 +48,20 @@ const FlatUsersView = (props: FlatUsersViewProps) => {
|
|||
|
||||
const isMobile = window.innerWidth <= 1490;
|
||||
|
||||
// Only show the detail pane once a real user is picked; until then let the
|
||||
// list span the full width so long emails aren't clipped by a reserved-but-
|
||||
// empty pane.
|
||||
const selectedUserObj = findUser(selectedUser);
|
||||
const hasSelection = selectedUser !== -1 && selectedUserObj !== undefined;
|
||||
|
||||
return (
|
||||
<Grid container spacing={1} sx={{ marginTop: '1px' }}>
|
||||
<Grid item xs={6} md={5}>
|
||||
<Grid item xs={12} md={hasSelection ? 5 : 12}>
|
||||
<Card>
|
||||
<Divider />
|
||||
<TableContainer sx={{ maxHeight: '520px', overflowY: 'auto' }}>
|
||||
<TableContainer
|
||||
sx={{ maxHeight: 'calc(100vh - 260px)', overflowY: 'auto' }}
|
||||
>
|
||||
<Table>
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
|
|
@ -114,14 +122,14 @@ const FlatUsersView = (props: FlatUsersViewProps) => {
|
|||
</TableContainer>
|
||||
</Card>
|
||||
</Grid>
|
||||
<Grid item xs={6} md={7}>
|
||||
{selectedUser && (
|
||||
{hasSelection && (
|
||||
<Grid item xs={12} md={7}>
|
||||
<User
|
||||
current_user={findUser(selectedUser)}
|
||||
current_user={selectedUserObj}
|
||||
fetchDataAgain={props.fetchDataAgain}
|
||||
></User>
|
||||
)}
|
||||
</Grid>
|
||||
</Grid>
|
||||
)}
|
||||
</Grid>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ import {
|
|||
Tab,
|
||||
Tabs,
|
||||
TextField,
|
||||
Tooltip,
|
||||
Typography,
|
||||
useTheme
|
||||
} from '@mui/material';
|
||||
|
|
@ -24,6 +25,7 @@ import Button from '@mui/material/Button';
|
|||
import axiosConfig from 'src/Resources/axiosConfig';
|
||||
import { InnovEnergyUser } from 'src/interfaces/UserTypes';
|
||||
import { TokenContext } from 'src/contexts/tokenContext';
|
||||
import { UserContext } from 'src/contexts/userContext';
|
||||
import { TabsContainerWrapper } from 'src/layouts/TabsContainerWrapper';
|
||||
import { FormattedMessage, useIntl } from 'react-intl';
|
||||
import UserAccess from '../ManageAccess/UserAccess';
|
||||
|
|
@ -43,6 +45,8 @@ function User(props: singleUserProps) {
|
|||
const [formValues, setFormValues] = useState(props.current_user);
|
||||
const tokencontext = useContext(TokenContext);
|
||||
const { removeToken } = tokencontext;
|
||||
const userContext = useContext(UserContext);
|
||||
const loggedInUser = userContext?.currentUser;
|
||||
const tabs = [
|
||||
{ value: 'user', label: intl.formatMessage({ id: 'user' }) },
|
||||
{ value: 'manage', label: intl.formatMessage({ id: 'accessManagement' }) }
|
||||
|
|
@ -161,6 +165,17 @@ function User(props: singleUserProps) {
|
|||
|
||||
const isMobile = window.innerWidth <= 1490;
|
||||
|
||||
// Mirror the backend delete guards: the main/root admin (the parentless top
|
||||
// user — parentId 0; its id is NOT necessarily 0) and your own account can
|
||||
// never be deleted, so disable the button and explain why.
|
||||
const isMainAdmin = formValues.parentId <= 0 || formValues.id === 0;
|
||||
const deleteDisabledReason = isMainAdmin
|
||||
? intl.formatMessage({ id: 'cannotDeleteMainAdmin' })
|
||||
: loggedInUser?.id === formValues.id
|
||||
? intl.formatMessage({ id: 'cannotDeleteSelf' })
|
||||
: '';
|
||||
const cannotDelete = deleteDisabledReason !== '';
|
||||
|
||||
return (
|
||||
<>
|
||||
{openModalDeleteUser && (
|
||||
|
|
@ -350,18 +365,20 @@ function User(props: singleUserProps) {
|
|||
defaultMessage="Apply Changes"
|
||||
/>
|
||||
</Button>
|
||||
<Button
|
||||
variant="contained"
|
||||
onClick={handleDelete}
|
||||
sx={{
|
||||
marginLeft: '10px'
|
||||
}}
|
||||
>
|
||||
<FormattedMessage
|
||||
id="delete_user"
|
||||
defaultMessage="Delete User"
|
||||
/>
|
||||
</Button>
|
||||
<Tooltip title={deleteDisabledReason}>
|
||||
<span style={{ marginLeft: '10px' }}>
|
||||
<Button
|
||||
variant="contained"
|
||||
onClick={handleDelete}
|
||||
disabled={cannotDelete}
|
||||
>
|
||||
<FormattedMessage
|
||||
id="delete_user"
|
||||
defaultMessage="Delete User"
|
||||
/>
|
||||
</Button>
|
||||
</span>
|
||||
</Tooltip>
|
||||
|
||||
{loading && (
|
||||
<CircularProgress
|
||||
|
|
|
|||
|
|
@ -20,8 +20,8 @@ import { UserType } from '../../../interfaces/UserTypes';
|
|||
function UsersSearch() {
|
||||
const intl = useIntl();
|
||||
const [searchTerm, setSearchTerm] = useState('');
|
||||
const { availableUsers, fetchAvailableUsers } = useContext(AccessContext);
|
||||
const [filteredData, setFilteredData] = useState(availableUsers);
|
||||
const { allUsers, fetchAllUsers } = useContext(AccessContext);
|
||||
const [filteredData, setFilteredData] = useState(allUsers);
|
||||
const [openModal, setOpenModal] = useState(false);
|
||||
const context = useContext(UserContext);
|
||||
const [userCreated, setUserCreated] = useState(false);
|
||||
|
|
@ -29,19 +29,19 @@ function UsersSearch() {
|
|||
const { currentUser } = context;
|
||||
|
||||
useEffect(() => {
|
||||
fetchAvailableUsers();
|
||||
fetchAllUsers();
|
||||
}, []);
|
||||
|
||||
const fetchDataAgain = () => {
|
||||
fetchAvailableUsers();
|
||||
fetchAllUsers();
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const filtered = availableUsers.filter((item) =>
|
||||
const filtered = allUsers.filter((item) =>
|
||||
item.name.toLowerCase().includes(searchTerm.toLowerCase())
|
||||
);
|
||||
setFilteredData(filtered);
|
||||
}, [searchTerm, availableUsers]);
|
||||
}, [searchTerm, allUsers]);
|
||||
|
||||
const handleSubmit = () => {
|
||||
setOpenModal(true);
|
||||
|
|
@ -50,7 +50,7 @@ function UsersSearch() {
|
|||
setOpenModal(false);
|
||||
setUserCreated(true);
|
||||
|
||||
fetchAvailableUsers();
|
||||
fetchAllUsers();
|
||||
|
||||
setTimeout(() => {
|
||||
setUserCreated(false);
|
||||
|
|
|
|||
|
|
@ -19,6 +19,8 @@ interface AccessContextProviderProps {
|
|||
accessibleInstallationsForUser: I_Installation[];
|
||||
availableUsers: InnovEnergyUser[];
|
||||
fetchAvailableUsers: () => Promise<void>;
|
||||
allUsers: InnovEnergyUser[];
|
||||
fetchAllUsers: () => Promise<void>;
|
||||
usersWithDirectAccess: InnovEnergyUser[];
|
||||
fetchUsersWithDirectAccessForResource: (
|
||||
tempresourceType: string,
|
||||
|
|
@ -53,6 +55,10 @@ export const AccessContext = createContext<AccessContextProviderProps>({
|
|||
fetchAvailableUsers: () => {
|
||||
return Promise.resolve();
|
||||
},
|
||||
allUsers: [],
|
||||
fetchAllUsers: () => {
|
||||
return Promise.resolve();
|
||||
},
|
||||
usersWithDirectAccess: [],
|
||||
fetchUsersWithDirectAccessForResource: () => Promise.resolve(),
|
||||
usersWithInheritedAccess: [],
|
||||
|
|
@ -80,6 +86,7 @@ const AccessContextProvider = ({ children }: { children: ReactNode }) => {
|
|||
InnovEnergyUser[]
|
||||
>([]);
|
||||
const [availableUsers, setAvailableUsers] = useState<InnovEnergyUser[]>([]);
|
||||
const [allUsers, setAllUsers] = useState<InnovEnergyUser[]>([]);
|
||||
const [accessibleInstallationsForUser, setAccessibleInstallationsForUser] =
|
||||
useState<I_Installation[]>([]);
|
||||
|
||||
|
|
@ -141,6 +148,13 @@ const AccessContextProvider = ({ children }: { children: ReactNode }) => {
|
|||
});
|
||||
};
|
||||
|
||||
// Admin-only: every user in the system (for the Users management page).
|
||||
const fetchAllUsers = async (): Promise<void> => {
|
||||
return axiosConfig.get('/GetAllUsers').then((res) => {
|
||||
setAllUsers(res.data);
|
||||
});
|
||||
};
|
||||
|
||||
const RevokeAccessFromResource = useCallback(
|
||||
async (
|
||||
resourceType: string,
|
||||
|
|
@ -187,6 +201,8 @@ const AccessContextProvider = ({ children }: { children: ReactNode }) => {
|
|||
accessibleInstallationsForUser,
|
||||
availableUsers,
|
||||
fetchAvailableUsers,
|
||||
allUsers,
|
||||
fetchAllUsers,
|
||||
usersWithDirectAccess,
|
||||
fetchUsersWithDirectAccessForResource,
|
||||
usersWithInheritedAccess,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,21 @@
|
|||
export interface BatteryAnomalySummary {
|
||||
installationId: number;
|
||||
lastDetectedAt: string;
|
||||
lastVoltage: number;
|
||||
lastBattery: string;
|
||||
count: number;
|
||||
}
|
||||
|
||||
export interface BatteryAnomalyLogEntry {
|
||||
id: number;
|
||||
installationId: number;
|
||||
batteryLabel: string;
|
||||
detectedAt: string;
|
||||
date: string;
|
||||
dateHour: string;
|
||||
voltage: number;
|
||||
soc: number;
|
||||
createdAt: string;
|
||||
}
|
||||
|
||||
export const VOLTAGE_ANOMALY_PRODUCTS = new Set<number>([2, 5]);
|
||||
|
|
@ -502,6 +502,8 @@
|
|||
"connectingToDevice": "Verbindung zum Gerät wird hergestellt...",
|
||||
"fetchingData": "Daten werden abgerufen...",
|
||||
"confirmDeleteUser": "Möchten Sie diesen Benutzer löschen?",
|
||||
"cannotDeleteSelf": "Sie können Ihr eigenes Konto nicht löschen",
|
||||
"cannotDeleteMainAdmin": "Das Hauptadministrator-Konto kann nicht gelöscht werden",
|
||||
"accessManagement": "Zugriffsverwaltung",
|
||||
"power": "Leistung",
|
||||
"voltage": "Spannung",
|
||||
|
|
@ -777,10 +779,8 @@
|
|||
"checklistStep11": "Software vor Ort verifiziert",
|
||||
"checklistStep12": "Installation online auf Monitor",
|
||||
"checklistStep13": "Kunde über Monitor-Konto und Reports informiert",
|
||||
"checklistStep14": "Benutzerkonto mit richtigen Ordnern und Zugriffen erstellt",
|
||||
"checklistStep15": "Kundennachverfolgung abgeschlossen, Feedback eingeholt",
|
||||
"checklistStep16": "Weitere Anliegen werden über das Ticket-System verfolgt",
|
||||
"checklistStep3Sub1": "Installations-Seriennummer",
|
||||
"checklistStep3Sub2": "Benutzerkonto mit richtigen Ordnern und Zugriffen erstellt",
|
||||
"checklistStep5Sub1": "Kundeninformationen (E-Mail, Adresse)",
|
||||
"checklistStep5Sub2": "Installationsinformationen (externes EMS, Stromanbieter, Datenerfassung)",
|
||||
"checklistStep5Sub3": "Batterie-Seriennummer",
|
||||
|
|
@ -801,6 +801,10 @@
|
|||
"checklistStep10Sub3": "Atef kontaktieren, falls externes EMS vorhanden ist",
|
||||
"checklistNoAttachments": "Noch keine Datei angehängt.",
|
||||
"setupProgress": "Monitor-Onboarding-Fortschritt",
|
||||
"batteryVoltageAnomaly": "Voll bei Unterspannung",
|
||||
"voltageAnomalyLogTitle": "Protokoll: voll bei Unterspannung",
|
||||
"voltageAnomalyTime": "Zeit",
|
||||
"noVoltageAnomalies": "Keine Anomalien erfasst",
|
||||
"checklistPhaseEmpty": "Nicht gestartet",
|
||||
"checklistPhasePreparation": "Vorbereitung",
|
||||
"checklistPhaseOnSite": "Vor Ort",
|
||||
|
|
|
|||
|
|
@ -250,6 +250,8 @@
|
|||
"connectingToDevice": "Connecting to the device...",
|
||||
"fetchingData": "Fetching data...",
|
||||
"confirmDeleteUser": "Do you want to delete this user?",
|
||||
"cannotDeleteSelf": "You cannot delete your own account",
|
||||
"cannotDeleteMainAdmin": "The main admin account cannot be deleted",
|
||||
"accessManagement": "Access Management",
|
||||
"power": "Power",
|
||||
"voltage": "Voltage",
|
||||
|
|
@ -525,10 +527,8 @@
|
|||
"checklistStep11": "Software verified on site",
|
||||
"checklistStep12": "Installation online on Monitor",
|
||||
"checklistStep13": "Customer informed about Monitor account and reports",
|
||||
"checklistStep14": "User account created with correct folders and access",
|
||||
"checklistStep15": "Customer follow-up completed, feedback collected",
|
||||
"checklistStep16": "Further issues tracked via Ticket system",
|
||||
"checklistStep3Sub1": "Installation serial number",
|
||||
"checklistStep3Sub2": "User account created with correct folders and access",
|
||||
"checklistStep5Sub1": "Customer information (email, address)",
|
||||
"checklistStep5Sub2": "Installation information (external EMS, grid provider, data collection)",
|
||||
"checklistStep5Sub3": "Battery serial number",
|
||||
|
|
@ -549,6 +549,10 @@
|
|||
"checklistStep10Sub3": "Contact Atef if there is external EMS",
|
||||
"checklistNoAttachments": "No file attached yet.",
|
||||
"setupProgress": "Monitor Onboarding Progress",
|
||||
"batteryVoltageAnomaly": "Full @ low voltage",
|
||||
"voltageAnomalyLogTitle": "Full-charge low-voltage log",
|
||||
"voltageAnomalyTime": "Time",
|
||||
"noVoltageAnomalies": "No anomalies recorded",
|
||||
"checklistPhaseEmpty": "Not started",
|
||||
"checklistPhasePreparation": "Preparation",
|
||||
"checklistPhaseOnSite": "On-site",
|
||||
|
|
|
|||
|
|
@ -502,6 +502,8 @@
|
|||
"connectingToDevice": "Connexion à l'appareil en cours...",
|
||||
"fetchingData": "Récupération des données...",
|
||||
"confirmDeleteUser": "Voulez-vous supprimer cet utilisateur ?",
|
||||
"cannotDeleteSelf": "Vous ne pouvez pas supprimer votre propre compte",
|
||||
"cannotDeleteMainAdmin": "Le compte de l'administrateur principal ne peut pas être supprimé",
|
||||
"accessManagement": "Gestion des accès",
|
||||
"power": "Puissance",
|
||||
"voltage": "Tension",
|
||||
|
|
@ -777,10 +779,8 @@
|
|||
"checklistStep11": "Logiciel vérifié sur site",
|
||||
"checklistStep12": "Installation en ligne sur Monitor",
|
||||
"checklistStep13": "Client informé du compte Monitor et des rapports",
|
||||
"checklistStep14": "Compte utilisateur créé avec les dossiers et accès corrects",
|
||||
"checklistStep15": "Suivi client effectué, retour recueilli",
|
||||
"checklistStep16": "Problèmes ultérieurs suivis via le système de tickets",
|
||||
"checklistStep3Sub1": "Numéro de série de l'installation",
|
||||
"checklistStep3Sub2": "Compte utilisateur créé avec les dossiers et accès corrects",
|
||||
"checklistStep5Sub1": "Informations client (e-mail, adresse)",
|
||||
"checklistStep5Sub2": "Informations d'installation (EMS externe, fournisseur réseau, collecte de données)",
|
||||
"checklistStep5Sub3": "Numéro de série de la batterie",
|
||||
|
|
@ -801,6 +801,10 @@
|
|||
"checklistStep10Sub3": "Contacter Atef en cas d'EMS externe",
|
||||
"checklistNoAttachments": "Aucun fichier joint pour le moment.",
|
||||
"setupProgress": "Progression d'onboarding Monitor",
|
||||
"batteryVoltageAnomaly": "Plein à basse tension",
|
||||
"voltageAnomalyLogTitle": "Journal : plein à basse tension",
|
||||
"voltageAnomalyTime": "Heure",
|
||||
"noVoltageAnomalies": "Aucune anomalie enregistrée",
|
||||
"checklistPhaseEmpty": "Non commencé",
|
||||
"checklistPhasePreparation": "Préparation",
|
||||
"checklistPhaseOnSite": "Sur site",
|
||||
|
|
|
|||
|
|
@ -502,6 +502,8 @@
|
|||
"connectingToDevice": "Connessione al dispositivo in corso...",
|
||||
"fetchingData": "Recupero dati in corso...",
|
||||
"confirmDeleteUser": "Vuoi eliminare questo utente?",
|
||||
"cannotDeleteSelf": "Non puoi eliminare il tuo account",
|
||||
"cannotDeleteMainAdmin": "L'account dell'amministratore principale non può essere eliminato",
|
||||
"accessManagement": "Gestione accessi",
|
||||
"power": "Potenza",
|
||||
"voltage": "Tensione",
|
||||
|
|
@ -777,10 +779,8 @@
|
|||
"checklistStep11": "Software verificato in sito",
|
||||
"checklistStep12": "Installazione online su Monitor",
|
||||
"checklistStep13": "Cliente informato su account Monitor e report",
|
||||
"checklistStep14": "Account utente creato con cartelle e accessi corretti",
|
||||
"checklistStep15": "Follow-up cliente completato, feedback raccolto",
|
||||
"checklistStep16": "Ulteriori problemi tracciati tramite il sistema di ticket",
|
||||
"checklistStep3Sub1": "Numero di serie dell'installazione",
|
||||
"checklistStep3Sub2": "Account utente creato con cartelle e accessi corretti",
|
||||
"checklistStep5Sub1": "Informazioni cliente (e-mail, indirizzo)",
|
||||
"checklistStep5Sub2": "Informazioni installazione (EMS esterno, fornitore di rete, raccolta dati)",
|
||||
"checklistStep5Sub3": "Numero di serie batteria",
|
||||
|
|
@ -801,6 +801,10 @@
|
|||
"checklistStep10Sub3": "Contattare Atef se è presente un EMS esterno",
|
||||
"checklistNoAttachments": "Nessun file allegato.",
|
||||
"setupProgress": "Avanzamento onboarding Monitor",
|
||||
"batteryVoltageAnomaly": "Pieno a bassa tensione",
|
||||
"voltageAnomalyLogTitle": "Registro: pieno a bassa tensione",
|
||||
"voltageAnomalyTime": "Ora",
|
||||
"noVoltageAnomalies": "Nessuna anomalia registrata",
|
||||
"checklistPhaseEmpty": "Non avviato",
|
||||
"checklistPhasePreparation": "Preparazione",
|
||||
"checklistPhaseOnSite": "In sito",
|
||||
|
|
|
|||
Loading…
Reference in New Issue