Files
timetracker/timetracker.Client/Components/Pages/SuperAdmin.razor
T
2026-06-24 23:48:04 +02:00

268 lines
14 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
@page "/superadmin"
@rendermode InteractiveWebAssembly
@attribute [Authorize(Policy = "AdminOnly")]
@using System.Net.Http.Json
@inject HttpClient Http
@inject ISnackbar Snackbar
@inject IDialogService DialogService
<PageTitle>Firmenverwaltung Timetracker SaaS</PageTitle>
@if (_loading)
{
<MudStack AlignItems="AlignItems.Center" Class="mt-16" Spacing="3">
<MudProgressCircular Color="Color.Primary" Indeterminate="true" Size="Size.Large" />
<MudText Color="Color.Secondary">Lade Firmen und Mandanten…</MudText>
</MudStack>
}
else
{
<MudStack Spacing="4">
@* ── Header Card ── *@
<MudPaper Elevation="4" Class="pa-6 rounded-xl"
Style="background: linear-gradient(135deg, var(--mud-palette-primary-darken) 0%, var(--mud-palette-primary) 100%); color: var(--mud-palette-primary-text);">
<MudStack Row="true" AlignItems="AlignItems.Center" Spacing="4">
<MudIcon Icon="@Icons.Material.Filled.Business" Style="color: var(--mud-palette-secondary); font-size:3rem" />
<MudStack Spacing="0">
<MudText Typo="Typo.h5" Style="color: var(--mud-palette-primary-text); font-weight:800">SuperAdmin Dashboard</MudText>
<MudText Typo="Typo.body2" Style="color: var(--mud-palette-primary-text); opacity: 0.72">
@_tenants.Count registrierte Firmen im System
</MudText>
</MudStack>
</MudStack>
</MudPaper>
@* ── Tenants Table ── *@
<MudCard Elevation="3" Class="rounded-xl overflow-hidden">
<MudCardContent Class="pa-0">
<MudTable Items="_tenants" Hover="true" Striped="true" Elevation="0" SortLabel="Sortieren">
<HeaderContent>
<MudTh Style="width: 50px;" />
<MudTh><MudTableSortLabel SortBy="new Func<TenantStats, object>(t => t.Id)">ID</MudTableSortLabel></MudTh>
<MudTh><MudTableSortLabel SortBy="new Func<TenantStats, object>(t => t.Name)">Name</MudTableSortLabel></MudTh>
<MudTh><MudTableSortLabel SortBy="new Func<TenantStats, object>(t => t.Subdomain)">Subdomain</MudTableSortLabel></MudTh>
<MudTh><MudTableSortLabel SortBy="new Func<TenantStats, object>(t => t.CreatedAt)">Erstellt am</MudTableSortLabel></MudTh>
<MudTh Style="text-align:center"><MudTableSortLabel SortBy="new Func<TenantStats, object>(t => t.UserCount)">Benutzer</MudTableSortLabel></MudTh>
<MudTh Style="text-align:center"><MudTableSortLabel SortBy="new Func<TenantStats, object>(t => t.WorkDaysCount)">Arbeitstage</MudTableSortLabel></MudTh>
<MudTh Style="text-align:center">Status</MudTh>
<MudTh Style="text-align:right">Aktionen</MudTh>
</HeaderContent>
<RowTemplate>
<MudTd>
<MudIconButton Icon="@(context.ShowDetails ? Icons.Material.Filled.ExpandLess : Icons.Material.Filled.ExpandMore)"
Size="Size.Small"
OnClick="@(() => context.ShowDetails = !context.ShowDetails)" />
</MudTd>
<MudTd DataLabel="ID">
<MudText Typo="Typo.body2" Color="Color.Secondary">@context.Id</MudText>
</MudTd>
<MudTd DataLabel="Name">
<MudText Typo="Typo.body1" Style="font-weight:600">@context.Name</MudText>
</MudTd>
<MudTd DataLabel="Subdomain">
<MudChip T="string" Color="Color.Primary" Variant="Variant.Outlined" Size="Size.Small">
@context.Subdomain
</MudChip>
</MudTd>
<MudTd DataLabel="Erstellt am">
<MudText Typo="Typo.body2">@context.CreatedAt.ToLocalTime().ToString("dd.MM.yyyy HH:mm")</MudText>
</MudTd>
<MudTd DataLabel="Benutzer" Style="text-align:center">
<MudBadge Content="@context.UserCount" Color="Color.Default" Overlap="false" Class="mx-2" />
</MudTd>
<MudTd DataLabel="Arbeitstage" Style="text-align:center">
<MudText Typo="Typo.body2">@context.WorkDaysCount</MudText>
</MudTd>
<MudTd DataLabel="Status" Style="text-align:center">
@if (context.IsApproved)
{
<MudChip T="string" Color="Color.Success" Size="Size.Small" Icon="@Icons.Material.Filled.Check">Aktiv</MudChip>
}
else
{
<MudChip T="string" Color="Color.Warning" Size="Size.Small" Icon="@Icons.Material.Filled.HourglassEmpty">Ausstehend</MudChip>
}
</MudTd>
<MudTd Style="text-align:right">
@if (!context.IsApproved)
{
<MudButton Variant="Variant.Filled"
Color="Color.Success"
Size="Size.Small"
StartIcon="@Icons.Material.Filled.CheckCircle"
OnClick="@(() => ApproveTenant(context))"
Class="mr-1">
Freigeben
</MudButton>
}
else
{
<MudButton Variant="Variant.Outlined"
Color="Color.Warning"
Size="Size.Small"
StartIcon="@Icons.Material.Filled.Block"
OnClick="@(() => ToggleActiveTenant(context))"
Class="mr-1">
Sperren
</MudButton>
}
<MudIconButton Icon="@Icons.Material.Filled.Delete"
Color="Color.Error"
Size="Size.Small"
OnClick="@(() => DeleteTenant(context))" />
</MudTd>
</RowTemplate>
<ChildRowContent>
@if (context.ShowDetails)
{
<MudTr>
<td colspan="9" style="background-color: var(--mud-palette-background-grey); padding: 16px;">
<MudPaper Elevation="1" Class="pa-4 rounded-lg">
<MudText Typo="Typo.subtitle2" Class="mb-3" Style="font-weight: 700;">
Benutzer für Firma: @context.Name
</MudText>
@if (context.Users == null || context.Users.Count == 0)
{
<MudText Typo="Typo.body2" Color="Color.Secondary">Keine Benutzer in dieser Firma registriert.</MudText>
}
else
{
<MudGrid Spacing="2">
@foreach (var user in context.Users)
{
<MudItem xs="12" sm="6" md="4">
<MudPaper Outlined="true" Class="pa-3 rounded-lg d-flex align-center justify-space-between" Style="background: white;">
<MudStack Row="true" AlignItems="AlignItems.Center" Spacing="2">
<MudAvatar Color="user.IsTenantAdmin ? Color.Primary : Color.Default" Size="Size.Small">
@user.Username.Substring(0, Math.Min(2, user.Username.Length)).ToUpper()
</MudAvatar>
<MudStack Spacing="0">
<MudText Typo="Typo.body2" Style="font-weight: 600;">@user.Username</MudText>
<MudText Typo="Typo.caption" Color="Color.Secondary">ID: @user.Id</MudText>
</MudStack>
</MudStack>
@if (user.IsTenantAdmin)
{
<MudChip T="string" Color="Color.Primary" Size="Size.Small" Variant="Variant.Text" Icon="@Icons.Material.Filled.AdminPanelSettings">Admin</MudChip>
}
else
{
<MudChip T="string" Color="Color.Default" Size="Size.Small" Variant="Variant.Text" Icon="@Icons.Material.Filled.Person">Mitarbeiter</MudChip>
}
</MudPaper>
</MudItem>
}
</MudGrid>
}
</MudPaper>
</td>
</MudTr>
}
</ChildRowContent>
</MudTable>
</MudCardContent>
</MudCard>
</MudStack>
}
@code {
private bool _loading = true;
private List<TenantStats> _tenants = [];
protected override async Task OnInitializedAsync()
{
await LoadTenants();
}
private async Task LoadTenants()
{
_loading = true;
try
{
_tenants = await Http.GetFromJsonAsync<List<TenantStats>>("api/superadmin/tenants") ?? [];
}
catch (Exception ex)
{
Snackbar.Add($"Fehler beim Laden der Firmen: {ex.Message}", Severity.Error);
}
finally
{
_loading = false;
}
}
private async Task ApproveTenant(TenantStats tenant)
{
try
{
var response = await Http.PostAsync($"api/superadmin/tenants/{tenant.Id}/approve", null);
if (response.IsSuccessStatusCode)
{
tenant.IsApproved = true;
Snackbar.Add($"Firma '{tenant.Name}' wurde erfolgreich freigegeben.", Severity.Success);
}
else
{
Snackbar.Add("Fehler bei der Freigabe.", Severity.Error);
}
}
catch (Exception ex)
{
Snackbar.Add($"Fehler: {ex.Message}", Severity.Error);
}
}
private async Task ToggleActiveTenant(TenantStats tenant)
{
try
{
var response = await Http.PostAsync($"api/superadmin/tenants/{tenant.Id}/toggle-active", null);
if (response.IsSuccessStatusCode)
{
tenant.IsApproved = !tenant.IsApproved;
var stateMsg = tenant.IsApproved ? "aktiviert" : "gesperrt";
Snackbar.Add($"Firma '{tenant.Name}' wurde {stateMsg}.", Severity.Info);
}
else
{
Snackbar.Add("Fehler beim Ändern des Status.", Severity.Error);
}
}
catch (Exception ex)
{
Snackbar.Add($"Fehler: {ex.Message}", Severity.Error);
}
}
private async Task DeleteTenant(TenantStats tenant)
{
bool? result = await DialogService.ShowMessageBoxAsync(
"Firma unwiderruflich löschen?",
$"Sind Sie sicher, dass Sie die Firma '{tenant.Name}' ({tenant.Subdomain}) inklusive ALLER zugehörigen Benutzer, Einstellungen und Zeiterfassungen unwiderruflich löschen möchten? Dieser Vorgang kann nicht rückgängig gemacht werden!",
yesText: "Löschen", cancelText: "Abbrechen");
if (result == true)
{
try
{
var response = await Http.DeleteAsync($"api/superadmin/tenants/{tenant.Id}");
if (response.IsSuccessStatusCode)
{
_tenants.Remove(tenant);
Snackbar.Add($"Firma '{tenant.Name}' wurde gelöscht.", Severity.Success);
}
else
{
var err = await response.Content.ReadAsStringAsync();
Snackbar.Add($"Fehler beim Löschen: {err}", Severity.Error);
}
}
catch (Exception ex)
{
Snackbar.Add($"Fehler: {ex.Message}", Severity.Error);
}
}
}
}