268 lines
14 KiB
Plaintext
268 lines
14 KiB
Plaintext
@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);
|
||
}
|
||
}
|
||
}
|
||
}
|