Compare commits
No commits in common. "ec0e8258b2a094e04845fdbc0c3eb01e9fd56959" and "e3248090da96e8b70bc92ecf2bc8010a5537f34a" have entirely different histories.
ec0e8258b2
...
e3248090da
|
|
@ -498,20 +498,6 @@ public class Controller : ControllerBase
|
||||||
.ToList();
|
.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))]
|
[HttpGet(nameof(GetAllInstallationsFromProduct))]
|
||||||
public ActionResult<IEnumerable<Installation>> GetAllInstallationsFromProduct(int product,Token authToken)
|
public ActionResult<IEnumerable<Installation>> GetAllInstallationsFromProduct(int product,Token authToken)
|
||||||
|
|
@ -3061,45 +3047,6 @@ public class Controller : ControllerBase
|
||||||
return Ok(summaries);
|
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) ──────────────
|
// ── On-Site Installer Checklist (sodistore home only) ──────────────
|
||||||
|
|
||||||
[HttpGet(nameof(GetOnSiteChecklistForInstallation))]
|
[HttpGet(nameof(GetOnSiteChecklistForInstallation))]
|
||||||
|
|
|
||||||
|
|
@ -1,30 +0,0 @@
|
||||||
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,8 +13,7 @@ public static class ChecklistStepDefinitions
|
||||||
new( 3, "Installation created on Monitor under correct product and folder",
|
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),
|
new( 4, "Gateway SD card configured, VPN and gateway name registered", NoSubtasks),
|
||||||
|
|
@ -65,5 +64,8 @@ public static class ChecklistStepDefinitions
|
||||||
new(11, "Software verified on site", NoSubtasks),
|
new(11, "Software verified on site", NoSubtasks),
|
||||||
new(12, "Installation online on Monitor", NoSubtasks),
|
new(12, "Installation online on Monitor", NoSubtasks),
|
||||||
new(13, "Customer informed about Monitor account and reports", 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,17 +368,11 @@ public static class SessionMethods
|
||||||
var sessionUser = session?.User;
|
var sessionUser = session?.User;
|
||||||
var originalUser = Db.GetUserById(editedUser?.Id);
|
var originalUser = Db.GetUserById(editedUser?.Id);
|
||||||
|
|
||||||
if (editedUser is null || sessionUser is null || originalUser is null)
|
return editedUser is not null
|
||||||
return false;
|
&& sessionUser is not null
|
||||||
|
&& originalUser is not null
|
||||||
// email must stay unique; pre-check to avoid hitting the DB [Unique] constraint (500)
|
&& sessionUser.UserType !=0
|
||||||
var emailOwner = Db.GetUserByEmail(editedUser.Email);
|
&& sessionUser.HasAccessTo(originalUser)
|
||||||
|
|
||||||
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
|
&& editedUser
|
||||||
.WithParentOf(originalUser) // prevent moving
|
.WithParentOf(originalUser) // prevent moving
|
||||||
.WithPasswordOf(originalUser)
|
.WithPasswordOf(originalUser)
|
||||||
|
|
@ -403,10 +397,7 @@ public static class SessionMethods
|
||||||
return sessionUser is not null
|
return sessionUser is not null
|
||||||
&& userToDelete is not null
|
&& userToDelete is not null
|
||||||
&& sessionUser.UserType !=0
|
&& sessionUser.UserType !=0
|
||||||
&& userToDelete.Id != 0 // belt: legacy root-id sentinel
|
&& sessionUser.HasAccessTo(userToDelete)
|
||||||
&& 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);
|
&& Db.Delete(userToDelete);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -48,11 +48,6 @@ public static partial class Db
|
||||||
return Insert(session);
|
return Insert(session);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static Boolean Create(BatteryVoltageAnomaly anomaly)
|
|
||||||
{
|
|
||||||
return Insert(anomaly);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static Boolean Create(InstallationAccess installationAccess)
|
public static Boolean Create(InstallationAccess installationAccess)
|
||||||
{
|
{
|
||||||
return Insert(installationAccess);
|
return Insert(installationAccess);
|
||||||
|
|
|
||||||
|
|
@ -50,9 +50,6 @@ public static partial class Db
|
||||||
public static TableQuery<OnSiteChecklistItem> OnSiteChecklistItems => Connection.Table<OnSiteChecklistItem>();
|
public static TableQuery<OnSiteChecklistItem> OnSiteChecklistItems => Connection.Table<OnSiteChecklistItem>();
|
||||||
public static TableQuery<OnSiteChecklistSignature> OnSiteChecklistSignatures => Connection.Table<OnSiteChecklistSignature>();
|
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()
|
public static void Init()
|
||||||
{
|
{
|
||||||
|
|
@ -101,9 +98,6 @@ public static partial class Db
|
||||||
// On-site installer checklist (sodistore home)
|
// On-site installer checklist (sodistore home)
|
||||||
Connection.CreateTable<OnSiteChecklistItem>();
|
Connection.CreateTable<OnSiteChecklistItem>();
|
||||||
Connection.CreateTable<OnSiteChecklistSignature>();
|
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
|
// One-time migration: normalize legacy long-form language values to ISO codes
|
||||||
|
|
@ -174,14 +168,6 @@ public static partial class Db
|
||||||
// raw-string JSON form preserving each row's existing checked state.
|
// raw-string JSON form preserving each row's existing checked state.
|
||||||
BackfillStep10Sub3();
|
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 —
|
// 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.
|
// the e-signature block now handles sign-off. Drop any rows seeded before removal.
|
||||||
Connection.Execute("DELETE FROM OnSiteChecklistItem WHERE StepNumber = 9");
|
Connection.Execute("DELETE FROM OnSiteChecklistItem WHERE StepNumber = 9");
|
||||||
|
|
@ -214,31 +200,6 @@ 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()
|
private static SQLiteConnection InitConnection()
|
||||||
{
|
{
|
||||||
var latestDb = new DirectoryInfo("DbBackups")
|
var latestDb = new DirectoryInfo("DbBackups")
|
||||||
|
|
@ -287,9 +248,6 @@ public static partial class Db
|
||||||
fileConnection.CreateTable<OnSiteChecklistItem>();
|
fileConnection.CreateTable<OnSiteChecklistItem>();
|
||||||
fileConnection.CreateTable<OnSiteChecklistSignature>();
|
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
|
// 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 ExternalEms = 'No' WHERE ExternalEms IS NULL OR ExternalEms = ''");
|
||||||
fileConnection.Execute("UPDATE Installation SET InstallationModel = '' WHERE InstallationModel IS NULL");
|
fileConnection.Execute("UPDATE Installation SET InstallationModel = '' WHERE InstallationModel IS NULL");
|
||||||
|
|
|
||||||
|
|
@ -164,18 +164,8 @@ public static partial class Db
|
||||||
|
|
||||||
Boolean DeleteUserAndHisDependencies()
|
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);
|
FolderAccess .Delete(u => u.UserId == user.Id);
|
||||||
InstallationAccess.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;
|
return Users.Delete(u => u.Id == user.Id) > 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -375,9 +365,4 @@ public static partial class Db
|
||||||
Backup();
|
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}).");
|
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,19 +272,4 @@ public static partial class Db
|
||||||
|
|
||||||
public static OnSiteChecklistSignature? GetOnSiteChecklistSignature(Int64 installationId)
|
public static OnSiteChecklistSignature? GetOnSiteChecklistSignature(Int64 installationId)
|
||||||
=> OnSiteChecklistSignatures.FirstOrDefault(s => s.InstallationId == 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,7 +36,6 @@ public static class Program
|
||||||
AlarmReviewService.StartDailyScheduler();
|
AlarmReviewService.StartDailyScheduler();
|
||||||
DailyIngestionService.StartScheduler();
|
DailyIngestionService.StartScheduler();
|
||||||
ReportAggregationService.StartScheduler();
|
ReportAggregationService.StartScheduler();
|
||||||
BatteryVoltageCheckService.StartScheduler();
|
|
||||||
var builder = WebApplication.CreateBuilder(args);
|
var builder = WebApplication.CreateBuilder(args);
|
||||||
|
|
||||||
RabbitMqManager.InitializeEnvironment();
|
RabbitMqManager.InitializeEnvironment();
|
||||||
|
|
|
||||||
|
|
@ -1,113 +0,0 @@
|
||||||
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; }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,144 +0,0 @@
|
||||||
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,22 +188,21 @@ public static class RabbitMqManager
|
||||||
|
|
||||||
Db.UpdateInstallationStatus(installationId, receivedStatusMessage.Status);
|
Db.UpdateInstallationStatus(installationId, receivedStatusMessage.Status);
|
||||||
|
|
||||||
// Automatic ticket creation on error/warning (alarm) state — disabled.
|
const int AlarmStatus = 2;
|
||||||
// const int AlarmStatus = 2;
|
var isSodistore = installation.Product is 2 or 3 or 4 or 5;
|
||||||
// var isSodistore = installation.Product is 2 or 3 or 4 or 5;
|
if (isSodistore
|
||||||
// if (isSodistore
|
&& prevStatus != AlarmStatus
|
||||||
// && prevStatus != AlarmStatus
|
&& receivedStatusMessage.Status == AlarmStatus)
|
||||||
// && receivedStatusMessage.Status == AlarmStatus)
|
{
|
||||||
// {
|
var prev = prevStatus;
|
||||||
// var prev = prevStatus;
|
var alarmsSnapshot = (IReadOnlyList<AlarmOrWarning>)
|
||||||
// var alarmsSnapshot = (IReadOnlyList<AlarmOrWarning>)
|
(receivedStatusMessage.Alarms?.ToList() ?? new List<AlarmOrWarning>());
|
||||||
// (receivedStatusMessage.Alarms?.ToList() ?? new List<AlarmOrWarning>());
|
_ = Task.Run(async () =>
|
||||||
// _ = Task.Run(async () =>
|
{
|
||||||
// {
|
try { await AutoTicketService.MaybeCreateForAlarmAsync(installation, prev, alarmsSnapshot); }
|
||||||
// try { await AutoTicketService.MaybeCreateForAlarmAsync(installation, prev, alarmsSnapshot); }
|
catch (Exception ex) { Console.WriteLine($"[AutoTicket] alarm failed for {installationId}: {ex.Message}"); }
|
||||||
// catch (Exception ex) { Console.WriteLine($"[AutoTicket] alarm failed for {installationId}: {ex.Message}"); }
|
});
|
||||||
// });
|
}
|
||||||
// }
|
|
||||||
|
|
||||||
//Console.WriteLine("----------------------------------------------");
|
//Console.WriteLine("----------------------------------------------");
|
||||||
//If the status has changed, update all the connected front-ends regarding this installation
|
//If the status has changed, update all the connected front-ends regarding this installation
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
import React, { useContext, useEffect, useMemo, useState } from 'react';
|
import React, { useEffect, useMemo, useState } from 'react';
|
||||||
import {
|
import {
|
||||||
Card,
|
|
||||||
Container,
|
Container,
|
||||||
Grid,
|
Grid,
|
||||||
Paper,
|
Paper,
|
||||||
|
|
@ -25,10 +24,6 @@ import {
|
||||||
ActiveCluster,
|
ActiveCluster,
|
||||||
getActiveClusters
|
getActiveClusters
|
||||||
} from '../Information/installationSetupUtils';
|
} 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 {
|
interface BatteryViewSodioHomeProps {
|
||||||
values: JSONRecordData;
|
values: JSONRecordData;
|
||||||
|
|
@ -52,22 +47,6 @@ function BatteryViewSodioHome(props: BatteryViewSodioHomeProps) {
|
||||||
|
|
||||||
const hasDevices = !!inverter?.Devices;
|
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 activeClusters: ActiveCluster[] = useMemo(() => {
|
||||||
const parsed = getActiveClusters(batterySerialNumbers || '');
|
const parsed = getActiveClusters(batterySerialNumbers || '');
|
||||||
if (parsed.length > 0) return parsed;
|
if (parsed.length > 0) return parsed;
|
||||||
|
|
@ -397,67 +376,6 @@ function BatteryViewSodioHome(props: BatteryViewSodioHomeProps) {
|
||||||
</TableContainer>
|
</TableContainer>
|
||||||
</Container>
|
</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 (total === 0) return 'checklistPhaseEmpty';
|
||||||
if (done >= total) return 'checklistPhaseComplete';
|
if (done >= total) return 'checklistPhaseComplete';
|
||||||
if (done <= 5) return 'checklistPhasePreparation';
|
if (done <= 5) return 'checklistPhasePreparation';
|
||||||
if (done <= 11) return 'checklistPhaseOnSite';
|
if (done <= 12) return 'checklistPhaseOnSite';
|
||||||
return 'checklistPhaseHandover';
|
return 'checklistPhaseHandover';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -550,7 +550,7 @@ const FlatInstallationView = (props: FlatInstallationViewProps) => {
|
||||||
return (
|
return (
|
||||||
<SetupProgress
|
<SetupProgress
|
||||||
done={summary?.done ?? 0}
|
done={summary?.done ?? 0}
|
||||||
total={summary?.total ?? 13}
|
total={summary?.total ?? 16}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
})()}
|
})()}
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,6 @@ import { useLocation, useNavigate } from 'react-router-dom';
|
||||||
import routes from '../../../Resources/routes.json';
|
import routes from '../../../Resources/routes.json';
|
||||||
import CancelIcon from '@mui/icons-material/Cancel';
|
import CancelIcon from '@mui/icons-material/Cancel';
|
||||||
import BuildIcon from '@mui/icons-material/Build';
|
import BuildIcon from '@mui/icons-material/Build';
|
||||||
import WarningAmberIcon from '@mui/icons-material/WarningAmber';
|
|
||||||
import { getDeviceTypeName } from '../Information/installationSetupUtils';
|
import { getDeviceTypeName } from '../Information/installationSetupUtils';
|
||||||
import { UserContext } from 'src/contexts/userContext';
|
import { UserContext } from 'src/contexts/userContext';
|
||||||
import { UserType } from 'src/interfaces/UserTypes';
|
import { UserType } from 'src/interfaces/UserTypes';
|
||||||
|
|
@ -28,10 +27,6 @@ import {
|
||||||
CHECKLIST_ENABLED_PRODUCTS,
|
CHECKLIST_ENABLED_PRODUCTS,
|
||||||
ChecklistSummary
|
ChecklistSummary
|
||||||
} from 'src/interfaces/ChecklistTypes';
|
} from 'src/interfaces/ChecklistTypes';
|
||||||
import {
|
|
||||||
BatteryAnomalySummary,
|
|
||||||
VOLTAGE_ANOMALY_PRODUCTS
|
|
||||||
} from 'src/interfaces/BatteryAnomalyTypes';
|
|
||||||
import SetupProgress from '../Checklist/SetupProgress';
|
import SetupProgress from '../Checklist/SetupProgress';
|
||||||
|
|
||||||
interface FlatInstallationViewProps {
|
interface FlatInstallationViewProps {
|
||||||
|
|
@ -49,18 +44,12 @@ const FlatInstallationView = (props: FlatInstallationViewProps) => {
|
||||||
const showChecklistColumn =
|
const showChecklistColumn =
|
||||||
currentUser?.userType === UserType.admin &&
|
currentUser?.userType === UserType.admin &&
|
||||||
CHECKLIST_ENABLED_PRODUCTS.has(props.product ?? -1);
|
CHECKLIST_ENABLED_PRODUCTS.has(props.product ?? -1);
|
||||||
const showAnomalyColumn =
|
|
||||||
currentUser?.userType === UserType.admin &&
|
|
||||||
VOLTAGE_ANOMALY_PRODUCTS.has(props.product ?? -1);
|
|
||||||
const isListViewPath =
|
const isListViewPath =
|
||||||
currentLocation.pathname === baseRoute + 'list' ||
|
currentLocation.pathname === baseRoute + 'list' ||
|
||||||
currentLocation.pathname === baseRoute + routes.list;
|
currentLocation.pathname === baseRoute + routes.list;
|
||||||
const [progressMap, setProgressMap] = useState<Map<number, ChecklistSummary>>(
|
const [progressMap, setProgressMap] = useState<Map<number, ChecklistSummary>>(
|
||||||
new Map()
|
new Map()
|
||||||
);
|
);
|
||||||
const [anomalyMap, setAnomalyMap] = useState<Map<number, BatteryAnomalySummary>>(
|
|
||||||
new Map()
|
|
||||||
);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!showChecklistColumn || !isListViewPath) return;
|
if (!showChecklistColumn || !isListViewPath) return;
|
||||||
|
|
@ -77,21 +66,6 @@ const FlatInstallationView = (props: FlatInstallationViewProps) => {
|
||||||
.catch(() => setProgressMap(new Map()));
|
.catch(() => setProgressMap(new Map()));
|
||||||
}, [showChecklistColumn, isListViewPath]);
|
}, [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(() => {
|
const sortedInstallations = useMemo(() => {
|
||||||
return [...props.installations].sort((a, b) => {
|
return [...props.installations].sort((a, b) => {
|
||||||
// Data-collection-disabled installations sink below everything (even offline).
|
// Data-collection-disabled installations sink below everything (even offline).
|
||||||
|
|
@ -184,14 +158,6 @@ const FlatInstallationView = (props: FlatInstallationViewProps) => {
|
||||||
/>
|
/>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
)}
|
)}
|
||||||
{showAnomalyColumn && (
|
|
||||||
<TableCell>
|
|
||||||
<FormattedMessage
|
|
||||||
id="batteryVoltageAnomaly"
|
|
||||||
defaultMessage="Full @ low voltage"
|
|
||||||
/>
|
|
||||||
</TableCell>
|
|
||||||
)}
|
|
||||||
</TableRow>
|
</TableRow>
|
||||||
</TableHead>
|
</TableHead>
|
||||||
<TableBody>
|
<TableBody>
|
||||||
|
|
@ -363,25 +329,12 @@ const FlatInstallationView = (props: FlatInstallationViewProps) => {
|
||||||
return (
|
return (
|
||||||
<SetupProgress
|
<SetupProgress
|
||||||
done={summary?.done ?? 0}
|
done={summary?.done ?? 0}
|
||||||
total={summary?.total ?? 13}
|
total={summary?.total ?? 16}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
})()}
|
})()}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
)}
|
)}
|
||||||
{showAnomalyColumn && (
|
|
||||||
<TableCell>
|
|
||||||
{anomalyMap.has(installation.id) && (
|
|
||||||
<WarningAmberIcon
|
|
||||||
style={{
|
|
||||||
width: '20px',
|
|
||||||
height: '20px',
|
|
||||||
color: '#f7b34d'
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</TableCell>
|
|
||||||
)}
|
|
||||||
</HoverableTableRow>
|
</HoverableTableRow>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|
|
||||||
|
|
@ -48,20 +48,12 @@ const FlatUsersView = (props: FlatUsersViewProps) => {
|
||||||
|
|
||||||
const isMobile = window.innerWidth <= 1490;
|
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 (
|
return (
|
||||||
<Grid container spacing={1} sx={{ marginTop: '1px' }}>
|
<Grid container spacing={1} sx={{ marginTop: '1px' }}>
|
||||||
<Grid item xs={12} md={hasSelection ? 5 : 12}>
|
<Grid item xs={6} md={5}>
|
||||||
<Card>
|
<Card>
|
||||||
<Divider />
|
<Divider />
|
||||||
<TableContainer
|
<TableContainer sx={{ maxHeight: '520px', overflowY: 'auto' }}>
|
||||||
sx={{ maxHeight: 'calc(100vh - 260px)', overflowY: 'auto' }}
|
|
||||||
>
|
|
||||||
<Table>
|
<Table>
|
||||||
<TableHead>
|
<TableHead>
|
||||||
<TableRow>
|
<TableRow>
|
||||||
|
|
@ -122,15 +114,15 @@ const FlatUsersView = (props: FlatUsersViewProps) => {
|
||||||
</TableContainer>
|
</TableContainer>
|
||||||
</Card>
|
</Card>
|
||||||
</Grid>
|
</Grid>
|
||||||
{hasSelection && (
|
<Grid item xs={6} md={7}>
|
||||||
<Grid item xs={12} md={7}>
|
{selectedUser && (
|
||||||
<User
|
<User
|
||||||
current_user={selectedUserObj}
|
current_user={findUser(selectedUser)}
|
||||||
fetchDataAgain={props.fetchDataAgain}
|
fetchDataAgain={props.fetchDataAgain}
|
||||||
></User>
|
></User>
|
||||||
</Grid>
|
|
||||||
)}
|
)}
|
||||||
</Grid>
|
</Grid>
|
||||||
|
</Grid>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,6 @@ import {
|
||||||
Tab,
|
Tab,
|
||||||
Tabs,
|
Tabs,
|
||||||
TextField,
|
TextField,
|
||||||
Tooltip,
|
|
||||||
Typography,
|
Typography,
|
||||||
useTheme
|
useTheme
|
||||||
} from '@mui/material';
|
} from '@mui/material';
|
||||||
|
|
@ -25,7 +24,6 @@ import Button from '@mui/material/Button';
|
||||||
import axiosConfig from 'src/Resources/axiosConfig';
|
import axiosConfig from 'src/Resources/axiosConfig';
|
||||||
import { InnovEnergyUser } from 'src/interfaces/UserTypes';
|
import { InnovEnergyUser } from 'src/interfaces/UserTypes';
|
||||||
import { TokenContext } from 'src/contexts/tokenContext';
|
import { TokenContext } from 'src/contexts/tokenContext';
|
||||||
import { UserContext } from 'src/contexts/userContext';
|
|
||||||
import { TabsContainerWrapper } from 'src/layouts/TabsContainerWrapper';
|
import { TabsContainerWrapper } from 'src/layouts/TabsContainerWrapper';
|
||||||
import { FormattedMessage, useIntl } from 'react-intl';
|
import { FormattedMessage, useIntl } from 'react-intl';
|
||||||
import UserAccess from '../ManageAccess/UserAccess';
|
import UserAccess from '../ManageAccess/UserAccess';
|
||||||
|
|
@ -45,8 +43,6 @@ function User(props: singleUserProps) {
|
||||||
const [formValues, setFormValues] = useState(props.current_user);
|
const [formValues, setFormValues] = useState(props.current_user);
|
||||||
const tokencontext = useContext(TokenContext);
|
const tokencontext = useContext(TokenContext);
|
||||||
const { removeToken } = tokencontext;
|
const { removeToken } = tokencontext;
|
||||||
const userContext = useContext(UserContext);
|
|
||||||
const loggedInUser = userContext?.currentUser;
|
|
||||||
const tabs = [
|
const tabs = [
|
||||||
{ value: 'user', label: intl.formatMessage({ id: 'user' }) },
|
{ value: 'user', label: intl.formatMessage({ id: 'user' }) },
|
||||||
{ value: 'manage', label: intl.formatMessage({ id: 'accessManagement' }) }
|
{ value: 'manage', label: intl.formatMessage({ id: 'accessManagement' }) }
|
||||||
|
|
@ -165,17 +161,6 @@ function User(props: singleUserProps) {
|
||||||
|
|
||||||
const isMobile = window.innerWidth <= 1490;
|
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 (
|
return (
|
||||||
<>
|
<>
|
||||||
{openModalDeleteUser && (
|
{openModalDeleteUser && (
|
||||||
|
|
@ -365,20 +350,18 @@ function User(props: singleUserProps) {
|
||||||
defaultMessage="Apply Changes"
|
defaultMessage="Apply Changes"
|
||||||
/>
|
/>
|
||||||
</Button>
|
</Button>
|
||||||
<Tooltip title={deleteDisabledReason}>
|
|
||||||
<span style={{ marginLeft: '10px' }}>
|
|
||||||
<Button
|
<Button
|
||||||
variant="contained"
|
variant="contained"
|
||||||
onClick={handleDelete}
|
onClick={handleDelete}
|
||||||
disabled={cannotDelete}
|
sx={{
|
||||||
|
marginLeft: '10px'
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<FormattedMessage
|
<FormattedMessage
|
||||||
id="delete_user"
|
id="delete_user"
|
||||||
defaultMessage="Delete User"
|
defaultMessage="Delete User"
|
||||||
/>
|
/>
|
||||||
</Button>
|
</Button>
|
||||||
</span>
|
|
||||||
</Tooltip>
|
|
||||||
|
|
||||||
{loading && (
|
{loading && (
|
||||||
<CircularProgress
|
<CircularProgress
|
||||||
|
|
|
||||||
|
|
@ -20,8 +20,8 @@ import { UserType } from '../../../interfaces/UserTypes';
|
||||||
function UsersSearch() {
|
function UsersSearch() {
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
const [searchTerm, setSearchTerm] = useState('');
|
const [searchTerm, setSearchTerm] = useState('');
|
||||||
const { allUsers, fetchAllUsers } = useContext(AccessContext);
|
const { availableUsers, fetchAvailableUsers } = useContext(AccessContext);
|
||||||
const [filteredData, setFilteredData] = useState(allUsers);
|
const [filteredData, setFilteredData] = useState(availableUsers);
|
||||||
const [openModal, setOpenModal] = useState(false);
|
const [openModal, setOpenModal] = useState(false);
|
||||||
const context = useContext(UserContext);
|
const context = useContext(UserContext);
|
||||||
const [userCreated, setUserCreated] = useState(false);
|
const [userCreated, setUserCreated] = useState(false);
|
||||||
|
|
@ -29,19 +29,19 @@ function UsersSearch() {
|
||||||
const { currentUser } = context;
|
const { currentUser } = context;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchAllUsers();
|
fetchAvailableUsers();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const fetchDataAgain = () => {
|
const fetchDataAgain = () => {
|
||||||
fetchAllUsers();
|
fetchAvailableUsers();
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const filtered = allUsers.filter((item) =>
|
const filtered = availableUsers.filter((item) =>
|
||||||
item.name.toLowerCase().includes(searchTerm.toLowerCase())
|
item.name.toLowerCase().includes(searchTerm.toLowerCase())
|
||||||
);
|
);
|
||||||
setFilteredData(filtered);
|
setFilteredData(filtered);
|
||||||
}, [searchTerm, allUsers]);
|
}, [searchTerm, availableUsers]);
|
||||||
|
|
||||||
const handleSubmit = () => {
|
const handleSubmit = () => {
|
||||||
setOpenModal(true);
|
setOpenModal(true);
|
||||||
|
|
@ -50,7 +50,7 @@ function UsersSearch() {
|
||||||
setOpenModal(false);
|
setOpenModal(false);
|
||||||
setUserCreated(true);
|
setUserCreated(true);
|
||||||
|
|
||||||
fetchAllUsers();
|
fetchAvailableUsers();
|
||||||
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
setUserCreated(false);
|
setUserCreated(false);
|
||||||
|
|
|
||||||
|
|
@ -19,8 +19,6 @@ interface AccessContextProviderProps {
|
||||||
accessibleInstallationsForUser: I_Installation[];
|
accessibleInstallationsForUser: I_Installation[];
|
||||||
availableUsers: InnovEnergyUser[];
|
availableUsers: InnovEnergyUser[];
|
||||||
fetchAvailableUsers: () => Promise<void>;
|
fetchAvailableUsers: () => Promise<void>;
|
||||||
allUsers: InnovEnergyUser[];
|
|
||||||
fetchAllUsers: () => Promise<void>;
|
|
||||||
usersWithDirectAccess: InnovEnergyUser[];
|
usersWithDirectAccess: InnovEnergyUser[];
|
||||||
fetchUsersWithDirectAccessForResource: (
|
fetchUsersWithDirectAccessForResource: (
|
||||||
tempresourceType: string,
|
tempresourceType: string,
|
||||||
|
|
@ -55,10 +53,6 @@ export const AccessContext = createContext<AccessContextProviderProps>({
|
||||||
fetchAvailableUsers: () => {
|
fetchAvailableUsers: () => {
|
||||||
return Promise.resolve();
|
return Promise.resolve();
|
||||||
},
|
},
|
||||||
allUsers: [],
|
|
||||||
fetchAllUsers: () => {
|
|
||||||
return Promise.resolve();
|
|
||||||
},
|
|
||||||
usersWithDirectAccess: [],
|
usersWithDirectAccess: [],
|
||||||
fetchUsersWithDirectAccessForResource: () => Promise.resolve(),
|
fetchUsersWithDirectAccessForResource: () => Promise.resolve(),
|
||||||
usersWithInheritedAccess: [],
|
usersWithInheritedAccess: [],
|
||||||
|
|
@ -86,7 +80,6 @@ const AccessContextProvider = ({ children }: { children: ReactNode }) => {
|
||||||
InnovEnergyUser[]
|
InnovEnergyUser[]
|
||||||
>([]);
|
>([]);
|
||||||
const [availableUsers, setAvailableUsers] = useState<InnovEnergyUser[]>([]);
|
const [availableUsers, setAvailableUsers] = useState<InnovEnergyUser[]>([]);
|
||||||
const [allUsers, setAllUsers] = useState<InnovEnergyUser[]>([]);
|
|
||||||
const [accessibleInstallationsForUser, setAccessibleInstallationsForUser] =
|
const [accessibleInstallationsForUser, setAccessibleInstallationsForUser] =
|
||||||
useState<I_Installation[]>([]);
|
useState<I_Installation[]>([]);
|
||||||
|
|
||||||
|
|
@ -148,13 +141,6 @@ 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(
|
const RevokeAccessFromResource = useCallback(
|
||||||
async (
|
async (
|
||||||
resourceType: string,
|
resourceType: string,
|
||||||
|
|
@ -201,8 +187,6 @@ const AccessContextProvider = ({ children }: { children: ReactNode }) => {
|
||||||
accessibleInstallationsForUser,
|
accessibleInstallationsForUser,
|
||||||
availableUsers,
|
availableUsers,
|
||||||
fetchAvailableUsers,
|
fetchAvailableUsers,
|
||||||
allUsers,
|
|
||||||
fetchAllUsers,
|
|
||||||
usersWithDirectAccess,
|
usersWithDirectAccess,
|
||||||
fetchUsersWithDirectAccessForResource,
|
fetchUsersWithDirectAccessForResource,
|
||||||
usersWithInheritedAccess,
|
usersWithInheritedAccess,
|
||||||
|
|
|
||||||
|
|
@ -1,21 +0,0 @@
|
||||||
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,8 +502,6 @@
|
||||||
"connectingToDevice": "Verbindung zum Gerät wird hergestellt...",
|
"connectingToDevice": "Verbindung zum Gerät wird hergestellt...",
|
||||||
"fetchingData": "Daten werden abgerufen...",
|
"fetchingData": "Daten werden abgerufen...",
|
||||||
"confirmDeleteUser": "Möchten Sie diesen Benutzer löschen?",
|
"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",
|
"accessManagement": "Zugriffsverwaltung",
|
||||||
"power": "Leistung",
|
"power": "Leistung",
|
||||||
"voltage": "Spannung",
|
"voltage": "Spannung",
|
||||||
|
|
@ -779,8 +777,10 @@
|
||||||
"checklistStep11": "Software vor Ort verifiziert",
|
"checklistStep11": "Software vor Ort verifiziert",
|
||||||
"checklistStep12": "Installation online auf Monitor",
|
"checklistStep12": "Installation online auf Monitor",
|
||||||
"checklistStep13": "Kunde über Monitor-Konto und Reports informiert",
|
"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",
|
"checklistStep3Sub1": "Installations-Seriennummer",
|
||||||
"checklistStep3Sub2": "Benutzerkonto mit richtigen Ordnern und Zugriffen erstellt",
|
|
||||||
"checklistStep5Sub1": "Kundeninformationen (E-Mail, Adresse)",
|
"checklistStep5Sub1": "Kundeninformationen (E-Mail, Adresse)",
|
||||||
"checklistStep5Sub2": "Installationsinformationen (externes EMS, Stromanbieter, Datenerfassung)",
|
"checklistStep5Sub2": "Installationsinformationen (externes EMS, Stromanbieter, Datenerfassung)",
|
||||||
"checklistStep5Sub3": "Batterie-Seriennummer",
|
"checklistStep5Sub3": "Batterie-Seriennummer",
|
||||||
|
|
@ -801,10 +801,6 @@
|
||||||
"checklistStep10Sub3": "Atef kontaktieren, falls externes EMS vorhanden ist",
|
"checklistStep10Sub3": "Atef kontaktieren, falls externes EMS vorhanden ist",
|
||||||
"checklistNoAttachments": "Noch keine Datei angehängt.",
|
"checklistNoAttachments": "Noch keine Datei angehängt.",
|
||||||
"setupProgress": "Monitor-Onboarding-Fortschritt",
|
"setupProgress": "Monitor-Onboarding-Fortschritt",
|
||||||
"batteryVoltageAnomaly": "Voll bei Unterspannung",
|
|
||||||
"voltageAnomalyLogTitle": "Protokoll: voll bei Unterspannung",
|
|
||||||
"voltageAnomalyTime": "Zeit",
|
|
||||||
"noVoltageAnomalies": "Keine Anomalien erfasst",
|
|
||||||
"checklistPhaseEmpty": "Nicht gestartet",
|
"checklistPhaseEmpty": "Nicht gestartet",
|
||||||
"checklistPhasePreparation": "Vorbereitung",
|
"checklistPhasePreparation": "Vorbereitung",
|
||||||
"checklistPhaseOnSite": "Vor Ort",
|
"checklistPhaseOnSite": "Vor Ort",
|
||||||
|
|
|
||||||
|
|
@ -250,8 +250,6 @@
|
||||||
"connectingToDevice": "Connecting to the device...",
|
"connectingToDevice": "Connecting to the device...",
|
||||||
"fetchingData": "Fetching data...",
|
"fetchingData": "Fetching data...",
|
||||||
"confirmDeleteUser": "Do you want to delete this user?",
|
"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",
|
"accessManagement": "Access Management",
|
||||||
"power": "Power",
|
"power": "Power",
|
||||||
"voltage": "Voltage",
|
"voltage": "Voltage",
|
||||||
|
|
@ -527,8 +525,10 @@
|
||||||
"checklistStep11": "Software verified on site",
|
"checklistStep11": "Software verified on site",
|
||||||
"checklistStep12": "Installation online on Monitor",
|
"checklistStep12": "Installation online on Monitor",
|
||||||
"checklistStep13": "Customer informed about Monitor account and reports",
|
"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",
|
"checklistStep3Sub1": "Installation serial number",
|
||||||
"checklistStep3Sub2": "User account created with correct folders and access",
|
|
||||||
"checklistStep5Sub1": "Customer information (email, address)",
|
"checklistStep5Sub1": "Customer information (email, address)",
|
||||||
"checklistStep5Sub2": "Installation information (external EMS, grid provider, data collection)",
|
"checklistStep5Sub2": "Installation information (external EMS, grid provider, data collection)",
|
||||||
"checklistStep5Sub3": "Battery serial number",
|
"checklistStep5Sub3": "Battery serial number",
|
||||||
|
|
@ -549,10 +549,6 @@
|
||||||
"checklistStep10Sub3": "Contact Atef if there is external EMS",
|
"checklistStep10Sub3": "Contact Atef if there is external EMS",
|
||||||
"checklistNoAttachments": "No file attached yet.",
|
"checklistNoAttachments": "No file attached yet.",
|
||||||
"setupProgress": "Monitor Onboarding Progress",
|
"setupProgress": "Monitor Onboarding Progress",
|
||||||
"batteryVoltageAnomaly": "Full @ low voltage",
|
|
||||||
"voltageAnomalyLogTitle": "Full-charge low-voltage log",
|
|
||||||
"voltageAnomalyTime": "Time",
|
|
||||||
"noVoltageAnomalies": "No anomalies recorded",
|
|
||||||
"checklistPhaseEmpty": "Not started",
|
"checklistPhaseEmpty": "Not started",
|
||||||
"checklistPhasePreparation": "Preparation",
|
"checklistPhasePreparation": "Preparation",
|
||||||
"checklistPhaseOnSite": "On-site",
|
"checklistPhaseOnSite": "On-site",
|
||||||
|
|
|
||||||
|
|
@ -502,8 +502,6 @@
|
||||||
"connectingToDevice": "Connexion à l'appareil en cours...",
|
"connectingToDevice": "Connexion à l'appareil en cours...",
|
||||||
"fetchingData": "Récupération des données...",
|
"fetchingData": "Récupération des données...",
|
||||||
"confirmDeleteUser": "Voulez-vous supprimer cet utilisateur ?",
|
"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",
|
"accessManagement": "Gestion des accès",
|
||||||
"power": "Puissance",
|
"power": "Puissance",
|
||||||
"voltage": "Tension",
|
"voltage": "Tension",
|
||||||
|
|
@ -779,8 +777,10 @@
|
||||||
"checklistStep11": "Logiciel vérifié sur site",
|
"checklistStep11": "Logiciel vérifié sur site",
|
||||||
"checklistStep12": "Installation en ligne sur Monitor",
|
"checklistStep12": "Installation en ligne sur Monitor",
|
||||||
"checklistStep13": "Client informé du compte Monitor et des rapports",
|
"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",
|
"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)",
|
"checklistStep5Sub1": "Informations client (e-mail, adresse)",
|
||||||
"checklistStep5Sub2": "Informations d'installation (EMS externe, fournisseur réseau, collecte de données)",
|
"checklistStep5Sub2": "Informations d'installation (EMS externe, fournisseur réseau, collecte de données)",
|
||||||
"checklistStep5Sub3": "Numéro de série de la batterie",
|
"checklistStep5Sub3": "Numéro de série de la batterie",
|
||||||
|
|
@ -801,10 +801,6 @@
|
||||||
"checklistStep10Sub3": "Contacter Atef en cas d'EMS externe",
|
"checklistStep10Sub3": "Contacter Atef en cas d'EMS externe",
|
||||||
"checklistNoAttachments": "Aucun fichier joint pour le moment.",
|
"checklistNoAttachments": "Aucun fichier joint pour le moment.",
|
||||||
"setupProgress": "Progression d'onboarding Monitor",
|
"setupProgress": "Progression d'onboarding Monitor",
|
||||||
"batteryVoltageAnomaly": "Plein à basse tension",
|
|
||||||
"voltageAnomalyLogTitle": "Journal : plein à basse tension",
|
|
||||||
"voltageAnomalyTime": "Heure",
|
|
||||||
"noVoltageAnomalies": "Aucune anomalie enregistrée",
|
|
||||||
"checklistPhaseEmpty": "Non commencé",
|
"checklistPhaseEmpty": "Non commencé",
|
||||||
"checklistPhasePreparation": "Préparation",
|
"checklistPhasePreparation": "Préparation",
|
||||||
"checklistPhaseOnSite": "Sur site",
|
"checklistPhaseOnSite": "Sur site",
|
||||||
|
|
|
||||||
|
|
@ -502,8 +502,6 @@
|
||||||
"connectingToDevice": "Connessione al dispositivo in corso...",
|
"connectingToDevice": "Connessione al dispositivo in corso...",
|
||||||
"fetchingData": "Recupero dati in corso...",
|
"fetchingData": "Recupero dati in corso...",
|
||||||
"confirmDeleteUser": "Vuoi eliminare questo utente?",
|
"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",
|
"accessManagement": "Gestione accessi",
|
||||||
"power": "Potenza",
|
"power": "Potenza",
|
||||||
"voltage": "Tensione",
|
"voltage": "Tensione",
|
||||||
|
|
@ -779,8 +777,10 @@
|
||||||
"checklistStep11": "Software verificato in sito",
|
"checklistStep11": "Software verificato in sito",
|
||||||
"checklistStep12": "Installazione online su Monitor",
|
"checklistStep12": "Installazione online su Monitor",
|
||||||
"checklistStep13": "Cliente informato su account Monitor e report",
|
"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",
|
"checklistStep3Sub1": "Numero di serie dell'installazione",
|
||||||
"checklistStep3Sub2": "Account utente creato con cartelle e accessi corretti",
|
|
||||||
"checklistStep5Sub1": "Informazioni cliente (e-mail, indirizzo)",
|
"checklistStep5Sub1": "Informazioni cliente (e-mail, indirizzo)",
|
||||||
"checklistStep5Sub2": "Informazioni installazione (EMS esterno, fornitore di rete, raccolta dati)",
|
"checklistStep5Sub2": "Informazioni installazione (EMS esterno, fornitore di rete, raccolta dati)",
|
||||||
"checklistStep5Sub3": "Numero di serie batteria",
|
"checklistStep5Sub3": "Numero di serie batteria",
|
||||||
|
|
@ -801,10 +801,6 @@
|
||||||
"checklistStep10Sub3": "Contattare Atef se è presente un EMS esterno",
|
"checklistStep10Sub3": "Contattare Atef se è presente un EMS esterno",
|
||||||
"checklistNoAttachments": "Nessun file allegato.",
|
"checklistNoAttachments": "Nessun file allegato.",
|
||||||
"setupProgress": "Avanzamento onboarding Monitor",
|
"setupProgress": "Avanzamento onboarding Monitor",
|
||||||
"batteryVoltageAnomaly": "Pieno a bassa tensione",
|
|
||||||
"voltageAnomalyLogTitle": "Registro: pieno a bassa tensione",
|
|
||||||
"voltageAnomalyTime": "Ora",
|
|
||||||
"noVoltageAnomalies": "Nessuna anomalia registrata",
|
|
||||||
"checklistPhaseEmpty": "Non avviato",
|
"checklistPhaseEmpty": "Non avviato",
|
||||||
"checklistPhasePreparation": "Preparazione",
|
"checklistPhasePreparation": "Preparazione",
|
||||||
"checklistPhaseOnSite": "In sito",
|
"checklistPhaseOnSite": "In sito",
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue