diff --git a/TrafagSalesExporter/Components/AdminAccessPanel.razor b/TrafagSalesExporter/Components/AdminAccessPanel.razor
new file mode 100644
index 0000000..43d4313
--- /dev/null
+++ b/TrafagSalesExporter/Components/AdminAccessPanel.razor
@@ -0,0 +1,91 @@
+@using TrafagSalesExporter.Services
+@inject IAdminAccessService AdminAccess
+@inject ISnackbar Snackbar
+@inject IUiTextService UiText
+
+
+
+
+ @T("Adminbereich ist geschützt. Bitte anmelden.", "Admin area is protected. Please sign in.")
+
+ @if (!AdminAccess.IsConfigured)
+ {
+
+ @T("Admin-Zugang ist noch nicht konfiguriert.", "Admin access is not configured yet.")
+
+ }
+
+
+
+ @T("Admin entsperren", "Unlock admin")
+
+
+
+
+
+
+
+
+
+
+ @T("Passwort speichern", "Save password")
+
+
+
+
+
+
+
+@code {
+ private string? _username;
+ private string? _password;
+ private string? _changeUsername;
+ private string? _currentPassword;
+ private string? _newPassword;
+ private string? _newPasswordRepeat;
+
+ private void Unlock()
+ {
+ if (!AdminAccess.TryUnlock(_username ?? string.Empty, _password ?? string.Empty))
+ {
+ Snackbar.Add(T("Admin-Anmeldung fehlgeschlagen.", "Admin sign-in failed."), Severity.Error);
+ return;
+ }
+
+ _password = string.Empty;
+ OnUnlocked.InvokeAsync();
+ }
+
+ private void ChangePassword()
+ {
+ if (string.IsNullOrWhiteSpace(_newPassword) || _newPassword.Length < 8)
+ {
+ Snackbar.Add(T("Das neue Passwort muss mindestens 8 Zeichen lang sein.", "The new password must be at least 8 characters long."), Severity.Warning);
+ return;
+ }
+
+ if (_newPassword != _newPasswordRepeat)
+ {
+ Snackbar.Add(T("Die neuen Passwörter stimmen nicht überein.", "The new passwords do not match."), Severity.Warning);
+ return;
+ }
+
+ if (!AdminAccess.TryChangePassword(_changeUsername ?? string.Empty, _currentPassword ?? string.Empty, _newPassword))
+ {
+ Snackbar.Add(T("Passwort konnte nicht geändert werden. Name oder aktuelles Passwort prüfen.", "Password could not be changed. Check the name or current password."), Severity.Error);
+ return;
+ }
+
+ _currentPassword = string.Empty;
+ _newPassword = string.Empty;
+ _newPasswordRepeat = string.Empty;
+ Snackbar.Add(T("Passwort wurde geändert.", "Password has been changed."), Severity.Success);
+ }
+
+ private string T(string german, string english) => UiText.Text(german, english);
+
+ [Parameter]
+ public EventCallback OnUnlocked { get; set; }
+}
diff --git a/TrafagSalesExporter/Components/FinanceCockpit/FinanceCockpitUnlockPanel.razor b/TrafagSalesExporter/Components/FinanceCockpit/FinanceCockpitUnlockPanel.razor
index d960431..14ecab1 100644
--- a/TrafagSalesExporter/Components/FinanceCockpit/FinanceCockpitUnlockPanel.razor
+++ b/TrafagSalesExporter/Components/FinanceCockpit/FinanceCockpitUnlockPanel.razor
@@ -23,12 +23,31 @@
StartIcon="@Icons.Material.Filled.LockOpen" Disabled="@(!FinanceAccess.IsConfigured)">
@T("Finance Cockpit entsperren", "Unlock Finance Cockpit")
+
+
+
+
+
+
+
+
+
+ @T("Passwort speichern", "Save password")
+
+
+
+
@code {
private string? _username;
private string? _password;
+ private string? _changeUsername;
+ private string? _currentPassword;
+ private string? _newPassword;
+ private string? _newPasswordRepeat;
private Task UnlockAsync()
{
@@ -43,5 +62,32 @@
return Task.CompletedTask;
}
+ private Task ChangePasswordAsync()
+ {
+ if (string.IsNullOrWhiteSpace(_newPassword) || _newPassword.Length < 8)
+ {
+ Snackbar.Add(T("Das neue Passwort muss mindestens 8 Zeichen lang sein.", "The new password must be at least 8 characters long."), Severity.Warning);
+ return Task.CompletedTask;
+ }
+
+ if (_newPassword != _newPasswordRepeat)
+ {
+ Snackbar.Add(T("Die neuen Passwörter stimmen nicht überein.", "The new passwords do not match."), Severity.Warning);
+ return Task.CompletedTask;
+ }
+
+ if (!FinanceAccess.TryChangePassword(_changeUsername ?? string.Empty, _currentPassword ?? string.Empty, _newPassword))
+ {
+ Snackbar.Add(T("Passwort konnte nicht geändert werden. Name oder aktuelles Passwort prüfen.", "Password could not be changed. Check the name or current password."), Severity.Error);
+ return Task.CompletedTask;
+ }
+
+ _currentPassword = string.Empty;
+ _newPassword = string.Empty;
+ _newPasswordRepeat = string.Empty;
+ Snackbar.Add(T("Passwort wurde geändert.", "Password has been changed."), Severity.Success);
+ return Task.CompletedTask;
+ }
+
private string T(string german, string english) => UiText.Text(german, english);
}
diff --git a/TrafagSalesExporter/Components/Layout/NavMenu.razor b/TrafagSalesExporter/Components/Layout/NavMenu.razor
index 428aea1..432d4f7 100644
--- a/TrafagSalesExporter/Components/Layout/NavMenu.razor
+++ b/TrafagSalesExporter/Components/Layout/NavMenu.razor
@@ -7,7 +7,7 @@
-
+
@T("Export Dashboard", "Export dashboard")
@@ -62,6 +62,9 @@
@T("HR KPI Schulung", "HR KPI training")
+
+ @T("Admin Bereich", "Admin area")
+
@code {
diff --git a/TrafagSalesExporter/Components/Pages/AdminSessions.razor b/TrafagSalesExporter/Components/Pages/AdminSessions.razor
new file mode 100644
index 0000000..089057d
--- /dev/null
+++ b/TrafagSalesExporter/Components/Pages/AdminSessions.razor
@@ -0,0 +1,84 @@
+@page "/admin/sessions"
+@attribute [Authorize(Policy = TrafagSalesExporter.Security.SecurityPolicies.AdminOnly)]
+@using TrafagSalesExporter.Services
+@inject IAccessSessionTracker SessionTracker
+@inject IAdminAccessService AdminAccess
+@inject IUiTextService UiText
+
+@T("Aktive Logins", "Active logins")
+
+@T("Aktive Logins", "Active logins")
+
+@if (!AdminAccess.IsUnlocked)
+{
+
+}
+else
+{
+
+
+
+ @T("HR-/Finance-Cockpit Sessions", "HR/Finance cockpit sessions")
+
+ @T("Gezählt werden App-interne Entsperrungen seit dem letzten App-Start.", "Counts app-internal unlocks since the last app start.")
+
+