diff --git a/flake.nix b/flake.nix
index 86ddc58..4245b6d 100644
--- a/flake.nix
+++ b/flake.nix
@@ -80,7 +80,9 @@
devShell = pkgs.mkShell {
buildInputs = with pkgs; [
mongodb-compass
-# jetbrains.webstorm
+ # jetbrains.webstorm
+ # jetbrains.rider
+ # dotnet-sdk_9
nodejs
nodePackages.prettier
];
diff --git a/testFrontend/SafeNSound.Demo/Layout/NavMenu.razor b/testFrontend/SafeNSound.Demo/Layout/NavMenu.razor
index 36501b1..a4d4522 100644
--- a/testFrontend/SafeNSound.Demo/Layout/NavMenu.razor
+++ b/testFrontend/SafeNSound.Demo/Layout/NavMenu.razor
@@ -24,6 +24,11 @@
<span class="bi bi-house-door-fill-nav-menu" aria-hidden="true"></span> Admin
</NavLink>
</div>
+ <div class="nav-item px-3">
+ <NavLink class="nav-link" href="/Tools" Match="NavLinkMatch.All">
+ <span class="bi bi-plus-square-fill-nav-menu" aria-hidden="true"></span> Tools
+ </NavLink>
+ </div>
</nav>
</div>
diff --git a/testFrontend/SafeNSound.Demo/Pages/Admin.razor b/testFrontend/SafeNSound.Demo/Pages/Admin.razor
index 0dcb1a3..952910d 100644
--- a/testFrontend/SafeNSound.Demo/Pages/Admin.razor
+++ b/testFrontend/SafeNSound.Demo/Pages/Admin.razor
@@ -5,8 +5,24 @@
return Task.CompletedTask;
})">Manage devices
</LinkButton>
+<LinkButton OnClick="@GetUserList">Get all users</LinkButton>
+
+<pre>
+ @foreach (var user in Enumerable.Reverse(UserList)) {
+ @(user + "\n")
+ }
+</pre>
@code {
-
-
+
+ List<string> UserList { get; set; } = [];
+
+ private async Task GetUserList() {
+ UserList.Clear();
+ await foreach (var userId in App.AdminClient.GetAllUserIdsEnumerable()) {
+ UserList.Add(userId);
+ StateHasChanged();
+ }
+ }
+
}
\ No newline at end of file
diff --git a/testFrontend/SafeNSound.Demo/Pages/Devices.razor b/testFrontend/SafeNSound.Demo/Pages/Devices.razor
index 5f90132..695c138 100644
--- a/testFrontend/SafeNSound.Demo/Pages/Devices.razor
+++ b/testFrontend/SafeNSound.Demo/Pages/Devices.razor
@@ -5,6 +5,7 @@
<hr/>
<LinkButton OnClick="@CreateDevice">Create new device</LinkButton>
<LinkButton OnClick="@ReloadDevices">Reload Devices</LinkButton>
+ <LinkButton OnClick="@Logout">Log out</LinkButton>
<LinkButton OnClick="@DeleteAccount">Delete account</LinkButton>
@foreach (var device in CurrentDevices) {
<div class="device-card">
@@ -77,5 +78,9 @@
private async Task DeleteAccount() {
await Client.DeleteAccount(Auth);
}
+
+ private async Task Logout() {
+ await Client.LogOut();
+ }
}
\ No newline at end of file
diff --git a/testFrontend/SafeNSound.Demo/Pages/Monitor.razor b/testFrontend/SafeNSound.Demo/Pages/Monitor.razor
index d78d60a..f948b69 100644
--- a/testFrontend/SafeNSound.Demo/Pages/Monitor.razor
+++ b/testFrontend/SafeNSound.Demo/Pages/Monitor.razor
@@ -6,16 +6,44 @@
return Task.CompletedTask;
})">Manage devices
</LinkButton>
-
+
@foreach (var user in AssignedUsers) {
- <p>Assigned user @user
+ <p>
+ <span>Assigned user @user</span>
@if (Alarms.ContainsKey(user)) {
- <pre>@Alarms[user].ToJson(indent: false)</pre>
+ <span>🔔</span>
+ <br/>
+ <i>@Alarms[user].Reason (@Alarms[user].CreatedAt)</i>
+ }
+ else {
+ <span>🔕</span>
}
- </p>
+ </p>
+ <LinkButton OnClick="() => { _ = ManageUser(user); return Task.CompletedTask; }">Manage</LinkButton>
}
}
+@if (!string.IsNullOrWhiteSpace(manageUserId)) {
+ <ModalWindow Title="Manage user">
+ <span>ID: @manageUserId</span><br/>
+ @if (Alarms.ContainsKey(manageUserId)) {
+ <span><b>User has alarm!</b></span>
+ <br/>
+ <i>@Alarms[manageUserId].Reason at @Alarms[manageUserId].CreatedAt.ToLocalTime()</i>
+ <LinkButton>Clear</LinkButton>
+ <br/>
+ }
+ <span>Budget: @manageUserBudget?.Amount EUR</span><br/>
+ <span>Add money: </span>
+ <FancyTextBox Value="@manageUserNewBudget?.Amount.ToString()"
+ ValueChanged="s => manageUserNewBudget.Amount = double.Parse(s)"/>
+ <span>, reason:</span>
+ <FancyTextBox @bind-Value="@manageUserNewBudget.Reason"/>
+ <LinkButton OnClick="@UpdateUserBudget">Add budget
+ </LinkButton>
+ </ModalWindow>
+}
+
@code {
bool _isInitialized = false;
@@ -28,4 +56,27 @@
_isInitialized = true;
}
+ string? manageUserId { get; set; }
+ BudgetWithHistory? manageUserBudget { get; set; }
+ BudgetHistoryEntry manageUserNewBudget { get; set; }
+
+ private async Task ManageUser(string user) {
+ manageUserId = user;
+ manageUserBudget = await App.MonitorClient.GetBudget(manageUserId);
+ manageUserBudget ??= new BudgetWithHistory {
+ Amount = 0,
+ History = []
+ };
+ manageUserNewBudget = new() {
+ Venue = App.MonitorAuth.Username
+ };
+
+ StateHasChanged();
+ }
+
+ private async Task UpdateUserBudget() {
+ await App.MonitorClient.AddBudget(manageUserId, manageUserNewBudget);
+ await ManageUser(manageUserId);
+ }
+
}
\ No newline at end of file
diff --git a/testFrontend/SafeNSound.Demo/Pages/Tools.razor b/testFrontend/SafeNSound.Demo/Pages/Tools.razor
new file mode 100644
index 0000000..f67f7ef
--- /dev/null
+++ b/testFrontend/SafeNSound.Demo/Pages/Tools.razor
@@ -0,0 +1,18 @@
+@page "/Tools"
+<h3>Tools</h3>
+<LinkButton OnClick="@MakeSuperAdmin">Assign more users to monitor</LinkButton>
+
+@code {
+
+ private async Task MakeSuperAdmin() {
+ await foreach (var user in App.AdminClient.GetAllUserIdsEnumerable()) {
+ try {
+ await App.MonitorClient.AddAssignedUser(user);
+ }
+ catch (Exception ex) {
+ Console.WriteLine($"Failed to assign user {user}: {ex.Message}");
+ }
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/testFrontend/SafeNSound.Demo/Pages/User.razor b/testFrontend/SafeNSound.Demo/Pages/User.razor
index cdffbe3..cfbfa53 100644
--- a/testFrontend/SafeNSound.Demo/Pages/User.razor
+++ b/testFrontend/SafeNSound.Demo/Pages/User.razor
@@ -16,6 +16,30 @@
<LinkButton Color="#FF0000" OnClick="@(() => RaiseAlarm(null))">Clear</LinkButton>
</span>
<br/>
+
+ <span>Budget: @Math.Round(Budget.Amount, 2)</span><br/>
+
+ <span>
+ Spend
+ <FancyTextBox Value="@ToSpend.Amount.ToString()" ValueChanged="s => ToSpend.Amount = double.Parse(s)"/>
+ EUR at
+ <FancyTextBox @bind-Value="@ToSpend.Venue"/>
+ for
+ <FancyTextBox @bind-Value="@ToSpend.Reason"/>
+ <LinkButton OnClick="@(() => SpendBudget(ToSpend))">Spend</LinkButton>
+ </span><br/>
+
+ <i>Spend history is comming soon!</i>
+ @* oops, forgot to use the DTO for getting own budget... *@
+ @* @foreach(var history in Budget.History) { *@
+ @* <div> *@
+ @* <span>@history.CreatedAt!.Value.ToLocalTime()</span> - *@
+ @* <span>@history.Amount EUR</span> - *@
+ @* <span>@history.Reason</span> - *@
+ @* <span>@history.Venue</span> *@
+ @* </div> *@
+ @* } *@
+
}
@if (Alarm != null) {
@@ -38,16 +62,22 @@
private WhoAmI WhoAmI { get; set; } = null!;
private AlarmDto? Alarm { get; set; } = null!;
+ private BudgetHistoryEntry ToSpend { get; set; } = new() {
+ Amount = 0,
+ Reason = string.Empty,
+ Venue = string.Empty
+ };
protected override async Task OnInitializedAsync() {
WhoAmI = await App.UserClient.WhoAmI();
- _isInitialized = true;
_ = PollAlarm();
+ Budget = await App.UserClient.GetBudget();
NavigationManager.LocationChanged += (sender, args) => {
if (args.Location != NavigationManager.Uri) {
- _running = false; // Stop polling when navigating away
+ _running = false;
}
};
+ _isInitialized = true;
}
private async Task PollAlarm() {
@@ -57,11 +87,19 @@
Alarm = newAlarm;
StateHasChanged();
}
+
+ var newBudget = await App.UserClient.GetBudget();
+ if (Math.Abs(Budget.Amount - newBudget.Amount) > 0.01) {
+ Budget = newBudget;
+ StateHasChanged();
+ }
await Task.Delay(1000);
}
}
+ public CurrentBalance Budget { get; set; }
+
private async Task RaiseAlarm(string? reason) {
if (string.IsNullOrWhiteSpace(reason))
await App.UserClient.DeleteAlarm();
@@ -80,4 +118,8 @@
~User() => ReleaseUnmanagedResources();
+ private async Task SpendBudget(BudgetHistoryEntry toSpend) {
+ await App.UserClient.SpendBudget(toSpend);
+ }
+
}
\ No newline at end of file
diff --git a/testFrontend/SafeNSound.Sdk/SafeNSoundClient.cs b/testFrontend/SafeNSound.Sdk/SafeNSoundClient.cs
index 3cd5d5c..b5bca12 100644
--- a/testFrontend/SafeNSound.Sdk/SafeNSoundClient.cs
+++ b/testFrontend/SafeNSound.Sdk/SafeNSoundClient.cs
@@ -66,12 +66,14 @@ public class SafeNSoundClient(SafeNSoundConfiguration config, string accessToken
res.EnsureSuccessStatusCode();
}
- public async Task<BudgetWithHistory> GetBudget(string userId = "@me") {
- var res = await HttpClient.GetAsync(
- userId == "@me"
- ? $"/budget/@me"
- : $"/user/{userId}/budget"
- );
+ public async Task<CurrentBalance> GetBudget() {
+ var res = await HttpClient.GetAsync($"/budget/@me");
+ res.EnsureSuccessStatusCode();
+ return (await res.Content.ReadFromJsonAsync<CurrentBalance>())!;
+ }
+
+ public async Task<BudgetWithHistory> GetBudget(string userId) {
+ var res = await HttpClient.GetAsync($"/user/{userId}/budget");
res.EnsureSuccessStatusCode();
return (await res.Content.ReadFromJsonAsync<BudgetWithHistory>())!;
}
@@ -143,7 +145,7 @@ public class SafeNSoundClient(SafeNSoundConfiguration config, string accessToken
public class AlarmDto {
[JsonPropertyName("_id")]
public string? Id { get; set; }
-
+
[JsonPropertyName("reason")]
public required string Reason { get; set; }
@@ -165,8 +167,13 @@ public class DeviceDto {
public DateTime LastSeen { get; set; }
}
+public class CurrentBalance {
+ [JsonPropertyName("currentBalance")]
+ public double Amount { get; set; }
+}
+
public class BudgetWithHistory {
- [JsonPropertyName("budget")]
+ [JsonPropertyName("balance")]
public double Amount { get; set; }
[JsonPropertyName("history")]
|