multi-tenant implementierung
This commit is contained in:
@@ -0,0 +1,267 @@
|
||||
@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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user