after refactor cleanup

This commit is contained in:
2025-11-28 11:21:22 +01:00
parent 5db6de1503
commit 07423023a0
305 changed files with 80 additions and 13326 deletions

View File

@@ -0,0 +1,42 @@
@using DiunaBI.UI.Shared.Services
@inject AuthService AuthService
@inject NavigationManager Navigation
@if (_isLoading)
{
<div class="d-flex justify-center align-center" style="height: 100vh;">
<MudProgressCircular Indeterminate="true" Size="Size.Large" />
</div>
}
else if (_isAuthenticated)
{
@ChildContent
}
@code {
[Parameter] public RenderFragment? ChildContent { get; set; }
private bool _isLoading = true;
private bool _isAuthenticated = false;
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender)
{
Console.WriteLine("AuthGuard: Checking authentication...");
_isAuthenticated = await AuthService.CheckAuthenticationAsync();
_isLoading = false;
Console.WriteLine($"AuthGuard: isAuthenticated={_isAuthenticated}");
if (!_isAuthenticated)
{
Console.WriteLine("AuthGuard: Redirecting to /login");
Navigation.NavigateTo("/login", replace: true);
}
StateHasChanged();
}
}
}

View File

@@ -0,0 +1,42 @@
@page "/dashboard"
@using DiunaBI.UI.Shared.Services
@using MudBlazor
@inject AuthService AuthService
@inject NavigationManager NavigationManager
@if (AuthService.IsAuthenticated && AuthService.CurrentUser != null)
{
<MudCard Class="mt-4" Elevation="2">
<MudCardHeader>
<CardHeaderAvatar>
@if (!string.IsNullOrEmpty(AuthService.CurrentUser.AvatarUrl))
{
<MudAvatar Size="Size.Large" Style="background: transparent;">
<img src="@AuthService.CurrentUser.AvatarUrl" alt="Avatar" style="width: 100%; height: 100%; object-fit: cover; border-radius: 50%;" />
</MudAvatar>
}
else
{
<MudAvatar Color="Color.Primary" Size="Size.Large">
@(AuthService.CurrentUser.FullName.Length > 0 ? AuthService.CurrentUser.FullName.Substring(0, 1) : "?")
</MudAvatar>
}
</CardHeaderAvatar>
<CardHeaderContent>
<MudText Typo="Typo.h6">@AuthService.CurrentUser.FullName</MudText>
<MudText Typo="Typo.body2">@AuthService.CurrentUser.Email</MudText>
</CardHeaderContent>
</MudCardHeader>
<MudCardContent>
<MudText Typo="Typo.body2">
✅ Signed in via Google
</MudText>
</MudCardContent>
</MudCard>
}
else
{
<MudAlert Severity="Severity.Warning" Class="mt-4">
You are not logged in
</MudAlert>
}

View File

@@ -0,0 +1,15 @@
@page "/"
@inject NavigationManager Navigation
@code
{
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender)
{
Navigation.NavigateTo("/dashboard");
}
await base.OnAfterRenderAsync(firstRender);
}
}

View File

@@ -0,0 +1,72 @@
@using MudBlazor.Internal
<MudExpansionPanels Class="mb-4">
<MudExpansionPanel Icon="@Icons.Material.Filled.FilterList"
Text="Filters"
Expanded="true">
<MudGrid>
<MudItem xs="12" sm="6" md="4">
<MudTextField @bind-Value="filterRequest.Search"
Label="Search"
Placeholder="Name, number..."
Immediate="true"
DebounceInterval="500"
OnDebounceIntervalElapsed="SearchLayers"
Clearable="true"/>
</MudItem>
<MudItem xs="12" sm="6" md="4">
<MudButton Variant="Variant.Outlined"
OnClick="ClearFilters"
StartIcon="Icons.Material.Filled.Clear">
Clear filters
</MudButton>
</MudItem>
</MudGrid>
</MudExpansionPanel>
</MudExpansionPanels>
<MudDivider Class="my-4"></MudDivider>
<MudTable Items="layers.Items"
Dense="true"
Hover="true"
Loading="isLoading"
LoadingProgressColor="Color.Info"
OnRowClick="@((TableRowClickEventArgs<LayerDto> args) => OnRowClick(args.Item))"
T="LayerDto"
Style="cursor: pointer;">
<HeaderContent>
<MudTh>Name</MudTh>
<MudTh>Type</MudTh>
</HeaderContent>
<RowTemplate>
<MudTd DataLabel="Name">@context.Name</MudTd>
<MudTd DataLabel="Type">@context.Type</MudTd>
</RowTemplate>
<NoRecordsContent>
<MudText>No layers to display</MudText>
</NoRecordsContent>
<LoadingContent>
Loading...
</LoadingContent>
</MudTable>
@if (layers.TotalCount > 0)
{
<MudGrid Class="mt-4" AlignItems="Center.Center">
<MudItem xs="12" sm="6">
<MudText Typo="Typo.body2">
Results @((layers.Page - 1) * layers.PageSize + 1) - @Math.Min(layers.Page * layers.PageSize, layers.TotalCount)
of @layers.TotalCount
</MudText>
</MudItem>
<MudItem xs="12" sm="6" Class="d-flex justify-end">
<MudPagination Count="layers.TotalPages"
Selected="layers.Page"
SelectedChanged="OnPageChanged"
ShowFirstButton="true"
ShowLastButton="true"/>
</MudItem>
</MudGrid>
}

View File

@@ -0,0 +1,65 @@
using DiunaBI.UI.Shared.Services;
using Microsoft.AspNetCore.Components;
using DiunaBI.Application.DTOModels;
using DiunaBI.Application.DTOModels.Common;
using MudBlazor;
namespace DiunaBI.UI.Shared.Components;
public partial class LayerListComponent : ComponentBase
{
[Inject] private LayerService LayerService { get; set; } = default!;
[Inject] private ISnackbar Snackbar { get; set; } = default!;
[Inject] private NavigationManager NavigationManager { get; set; } = default!;
private PagedResult<LayerDto> layers = new();
private LayerFilterRequest filterRequest = new();
private bool isLoading = false;
protected override async Task OnInitializedAsync()
{
await LoadLayers();
}
private async Task LoadLayers()
{
isLoading = true;
try
{
layers = await LayerService.GetLayersAsync(filterRequest);
}
catch (Exception ex)
{
Console.WriteLine($"Loading layers failed: {ex.Message}");
}
finally
{
isLoading = false;
}
}
private async Task SearchLayers()
{
filterRequest.Page = 1;
await LoadLayers();
}
private async Task OnPageChanged(int page)
{
filterRequest.Page = page;
await LoadLayers();
}
private async Task ClearFilters()
{
filterRequest = new LayerFilterRequest();
await LoadLayers();
}
private void OnRowClick(LayerDto layer)
{
NavigationManager.NavigateTo($"/layers/{layer.Id}");
}
}

View File

@@ -0,0 +1,200 @@
@using DiunaBI.UI.Shared.Services
@using Microsoft.Extensions.Configuration
@inject IJSRuntime JS
@inject IConfiguration Configuration
@inject AuthService AuthService
@inject NavigationManager NavigationManager
@inject IGoogleAuthService? GoogleAuthService
<MudCard Class="login-card" Elevation="8">
<MudCardContent Class="pa-8 d-flex flex-column align-center">
<MudText Typo="Typo.h4" Class="mb-4">Welcome to DiunaBI</MudText>
<MudText Typo="Typo.body1" Class="mb-6 text-center">
Sign in using your Google account
</MudText>
<MudButton
Variant="Variant.Filled"
StartIcon="@Icons.Custom.Brands.Google"
Size="Size.Large"
OnClick="HandleGoogleSignIn"
Disabled="@_isLoading">
@if (_isLoading)
{
<MudProgressCircular Class="mr-3" Size="Size.Small" Indeterminate="true"></MudProgressCircular>
<span>Verifying...</span>
}
else
{
<span>Sign in with Google</span>
}
</MudButton>
@if (!string.IsNullOrEmpty(_errorMessage))
{
<MudAlert Severity="Severity.Error" Class="mt-4" Dense="true">
@_errorMessage
</MudAlert>
}
</MudCardContent>
</MudCard>
@code {
private bool _isLoading = false;
private string _errorMessage = string.Empty;
private static LoginCard? _instance;
private bool _isInitialized = false;
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender)
{
_instance = this;
// Initialize JavaScript Google SDK for web (both null service and WebGoogleAuthService)
// Skip only for mobile platform-specific auth (MobileGoogleAuthService)
var isMobileAuth = GoogleAuthService != null && GoogleAuthService.GetType().Name == "MobileGoogleAuthService";
if (!isMobileAuth)
{
await InitializeGoogleSignIn();
}
else
{
Console.WriteLine("📱 Using platform-specific Google auth service");
_isInitialized = true;
}
}
}
private async Task InitializeGoogleSignIn()
{
try
{
if (_isInitialized) return;
var clientId = Configuration["GoogleAuth:ClientId"];
Console.WriteLine($"🔍 Reading GoogleAuth:ClientId from configuration: '{clientId}'");
if (string.IsNullOrEmpty(clientId))
{
_errorMessage = "Google ClientId is not configured in appsettings.";
Console.Error.WriteLine("❌ Google ClientId is NULL or EMPTY in configuration!");
return;
}
Console.WriteLine($"✅ Calling initGoogleSignIn with clientId: {clientId}");
await JS.InvokeVoidAsync("initGoogleSignIn", clientId);
_isInitialized = true;
}
catch (Exception ex)
{
_errorMessage = "Google Sign-In initialization error.";
Console.Error.WriteLine($"❌ Google Sign-In initialization error: {ex.Message}");
Console.Error.WriteLine($"Stack trace: {ex.StackTrace}");
}
}
private async Task HandleGoogleSignIn()
{
try
{
_isLoading = true;
_errorMessage = string.Empty;
StateHasChanged();
// Use platform-specific auth if available (mobile), otherwise use JavaScript (web)
if (GoogleAuthService != null)
{
Console.WriteLine("📱 Using mobile Google auth");
var success = await GoogleAuthService.SignInAsync();
if (success)
{
Console.WriteLine("✅ Mobile auth successful, navigating...");
NavigationManager.NavigateTo("/dashboard", replace: true);
}
else
{
_errorMessage = "Login failed. Please try again";
_isLoading = false;
StateHasChanged();
}
}
else
{
Console.WriteLine("🌐 Using web JavaScript Google auth");
await JS.InvokeVoidAsync("requestGoogleSignIn");
}
}
catch (Exception ex)
{
Console.Error.WriteLine($"❌ HandleGoogleSignIn error: {ex.Message}");
_errorMessage = "Login error. Please try again";
_isLoading = false;
StateHasChanged();
}
}
[JSInvokable]
public static async Task OnGoogleSignInSuccess(string googleCredential, string fullName, string email, string avatarUrl)
{
Console.WriteLine($"=== OnGoogleSignInSuccess: {email} ===");
if (_instance != null)
{
try
{
// Waliduj użytkownika w backendzie DiunaBI
var (success, errorMessage) = await _instance.AuthService.ValidateWithBackendAsync(
googleCredential, fullName, email, avatarUrl);
if (success)
{
Console.WriteLine("✅ User validated, navigating to dashboard");
_instance._isLoading = false;
_instance._errorMessage = string.Empty;
_instance.NavigationManager.NavigateTo("/dashboard", replace: true);
}
else
{
Console.WriteLine($"❌ Validation failed: {errorMessage}");
_instance._isLoading = false;
_instance._errorMessage = errorMessage ?? "Login failed.";
}
await _instance.InvokeAsync(() => _instance.StateHasChanged());
}
catch (Exception ex)
{
Console.Error.WriteLine($"❌ OnGoogleSignInSuccess error: {ex.Message}");
_instance._isLoading = false;
_instance._errorMessage = "User verification error.";
await _instance.InvokeAsync(() => _instance.StateHasChanged());
}
}
}
[JSInvokable]
public static async Task OnGoogleSignInError(string error)
{
Console.WriteLine($"Google SignIn Error: {error}");
if (_instance != null)
{
_instance._isLoading = false;
_instance._errorMessage = "Google login error. Please try again";
await _instance.InvokeAsync(() => _instance.StateHasChanged());
}
}
}
<style>
.login-card {
max-width: 400px;
width: 100%;
background: rgba(255, 255, 255, 0.95);
backdrop-filter: blur(10px);
}
</style>

View File

@@ -0,0 +1,10 @@
@using Microsoft.AspNetCore.Components.Routing
@using MudBlazor
<MudNavMenu>
<MudNavLink href="dashboard" Icon="@Icons.Material.Filled.Dashboard" Match="NavLinkMatch.All">
Dashboard
</MudNavLink>
<MudNavLink Href="layers" Icon="@Icons.Material.Filled.List" Match="NavLinkMatch.All">
Layers
</MudNavLink>
</MudNavMenu>

View File

@@ -0,0 +1,18 @@
@using Microsoft.AspNetCore.Components.Routing
@using MudBlazor
<Router AppAssembly="@typeof(Routes).Assembly">
<Found Context="routeData">
<RouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)" />
<FocusOnNavigate RouteData="routeData" Selector="h1" />
</Found>
<NotFound>
<LayoutView Layout="@typeof(MainLayout)">
<MudCard Elevation="0">
<MudText Typo="Typo.h6">
Strona nieznaleziona.
</MudText>
</MudCard>
</LayoutView>
</NotFound>
</Router>

View File

@@ -0,0 +1,30 @@
<Project Sdk="Microsoft.NET.Sdk.Razor">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
<ItemGroup>
<SupportedPlatform Include="browser"/>
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Components.Web" Version="10.0.0"/>
<PackageReference Include="MudBlazor" Version="8.0.0"/>
<PackageReference Include="Microsoft.AspNetCore.WebUtilities" Version="10.0.0" />
<PackageReference Include="Microsoft.Extensions.Http" Version="10.0.0" />
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="10.0.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\DiunaBI.Application\DiunaBI.Application.csproj" />
</ItemGroup>
<ItemGroup>
<Folder Include="Interfaces\" />
<Folder Include="wwwroot\" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,8 @@
@inherits LayoutComponentBase
<MudThemeProvider/>
<MudDialogProvider/>
<MudSnackbarProvider/>
@Body

View File

@@ -0,0 +1,42 @@
using Microsoft.Extensions.DependencyInjection;
using DiunaBI.UI.Shared.Services;
using DiunaBI.UI.Shared.Handlers;
namespace DiunaBI.UI.Shared.Extensions;
public static class ServiceCollectionExtensions
{
public static IServiceCollection AddSharedServices(this IServiceCollection services, string apiBaseUrl)
{
// HttpClient for API calls with logging
// Ensure BaseAddress ends with / for proper relative URL resolution
var baseUri = apiBaseUrl.EndsWith('/') ? apiBaseUrl : apiBaseUrl + "/";
Console.WriteLine($"🔧 Configuring HttpClient with BaseAddress: {baseUri}");
services.AddTransient<HttpLoggingHandler>();
// Configure named HttpClient with logging handler
services.AddHttpClient("DiunaBI", client =>
{
client.BaseAddress = new Uri(baseUri);
Console.WriteLine($"✅ HttpClient BaseAddress set to: {client.BaseAddress}");
})
.AddHttpMessageHandler<HttpLoggingHandler>();
// Register a scoped HttpClient factory that services will use
services.AddScoped<HttpClient>(sp =>
{
var factory = sp.GetRequiredService<IHttpClientFactory>();
var client = factory.CreateClient("DiunaBI");
Console.WriteLine($"🏭 HttpClient created from factory. BaseAddress: {client.BaseAddress}");
return client;
});
// Services
services.AddScoped<AuthService>();
services.AddScoped<LayerService>();
return services;
}
}

View File

@@ -0,0 +1,28 @@
namespace DiunaBI.UI.Shared.Handlers;
public class HttpLoggingHandler : DelegatingHandler
{
protected override async Task<HttpResponseMessage> SendAsync(
HttpRequestMessage request,
CancellationToken cancellationToken)
{
Console.WriteLine($"🌐 HTTP Request: {request.Method} {request.RequestUri}");
Console.WriteLine($" BaseAddress: {request.RequestUri?.GetLeftPart(UriPartial.Authority)}");
Console.WriteLine($" Path: {request.RequestUri?.PathAndQuery}");
if (request.Headers.Authorization != null)
{
Console.WriteLine($" Auth: {request.Headers.Authorization.Scheme} {request.Headers.Authorization.Parameter?[..Math.Min(20, request.Headers.Authorization.Parameter?.Length ?? 0)]}...");
}
else
{
Console.WriteLine($" Auth: None");
}
var response = await base.SendAsync(request, cancellationToken);
Console.WriteLine($" Response: {(int)response.StatusCode} {response.StatusCode}");
return response;
}
}

View File

@@ -0,0 +1,81 @@
@using MudBlazor
@inherits LayoutComponentBase
<AuthGuard>
<MudThemeProvider Theme="_theme"/>
<MudDialogProvider/>
<MudSnackbarProvider/>
<MudLayout>
<MudBreakpointProvider OnBreakpointChanged="OnBreakpointChanged"></MudBreakpointProvider>
<MudAppBar Elevation="0">
<MudIconButton
Icon="@Icons.Material.Filled.Menu"
Color="Color.Inherit"
Edge="Edge.Start"
OnClick="ToggleDrawer"
Class="mud-hidden-md-up"/>
<MudSpacer/>
<MudText Typo="Typo.h6">DiunaBI</MudText>
</MudAppBar>
<MudDrawer @bind-Open="_drawerOpen"
Anchor="Anchor.Start"
Variant="@_drawerVariant"
Elevation="1"
ClipMode="DrawerClipMode.Always"
Class="mud-width-250">
<MudNavMenu>
<MudNavLink Href="/dashboard" Icon="@Icons.Material.Filled.Dashboard">Dashboard</MudNavLink>
<MudNavLink Href="/layers" Icon="@Icons.Material.Filled.Inventory">Layers</MudNavLink>
</MudNavMenu>
</MudDrawer>
<MudMainContent>
<MudContainer MaxWidth="MaxWidth.ExtraExtraLarge" Class="my-4">
@Body
</MudContainer>
</MudMainContent>
</MudLayout>
</AuthGuard>
@code {
private bool _drawerOpen = true;
private DrawerVariant _drawerVariant = DrawerVariant.Persistent;
private MudTheme _theme = new MudTheme()
{
PaletteLight = new PaletteLight()
{
Primary = "#e7163d",
PrimaryDarken = "#c01234",
PrimaryLighten = "#f04366",
Secondary = "#424242",
AppbarBackground = "#e7163d",
}
};
void ToggleDrawer()
{
Console.WriteLine($"ToogleDrawer clickkk {DateTime.Now}");
_drawerOpen = !_drawerOpen;
}
private void OnBreakpointChanged(Breakpoint breakpoint)
{
if (breakpoint < Breakpoint.Md)
{
_drawerVariant = DrawerVariant.Temporary;
_drawerOpen = false;
}
else
{
_drawerVariant = DrawerVariant.Persistent;
_drawerOpen = true;
}
StateHasChanged();
}
}

View File

@@ -0,0 +1,112 @@
@page "/layers/{id:guid}"
@using DiunaBI.UI.Shared.Services
@using DiunaBI.Application.DTOModels
@using MudBlazor
@inject LayerService LayerService
@inject NavigationManager NavigationManager
<MudCard>
<MudCardHeader>
<CardHeaderContent>
<MudText Typo="Typo.h5">Layer Details</MudText>
</CardHeaderContent>
<CardHeaderActions>
<MudButton Variant="Variant.Text" OnClick="Export">Export</MudButton>
@if (layer != null && layer.Type == LayerType.Administration)
{
<MudButton Variant="Variant.Text" Href="@($"/layers/edit/{layer.Id}/duplicate")">Duplicate</MudButton>
<MudButton Variant="Variant.Text" Href="@($"/layers/edit/{layer.Id}")">Edit</MudButton>
}
@if (layer != null && layer.Type == LayerType.Processed)
{
<MudButton Variant="Variant.Text" OnClick="ProcessLayer">Process Layer</MudButton>
}
</CardHeaderActions>
</MudCardHeader>
<MudCardContent>
@if (isLoading)
{
<MudProgressLinear Color="Color.Primary" Indeterminate="true" />
}
else if (layer == null)
{
<MudAlert Severity="Severity.Error">Layer not found</MudAlert>
}
else
{
<MudGrid>
<MudItem xs="12" md="6">
<MudTextField @bind-Value="layer.Name"
Label="Name"
Variant="Variant.Outlined"
ReadOnly="true"
FullWidth="true"/>
</MudItem>
<MudItem xs="12" md="6">
@if (layer.IsCancelled)
{
<MudAlert Severity="Severity.Warning" Dense="true">
This layer is cancelled. Will not be used in any further processing.
</MudAlert>
}
</MudItem>
<MudItem xs="12" md="6">
<MudTextField Value="@layer.CreatedAt.ToString("g")"
Label="Created"
Variant="Variant.Outlined"
ReadOnly="true"
FullWidth="true"
Adornment="Adornment.End"
AdornmentText="@(layer.CreatedBy?.Username ?? "")"/>
</MudItem>
<MudItem xs="12" md="6">
<MudTextField Value="@layer.ModifiedAt.ToString("g")"
Label="Modified"
Variant="Variant.Outlined"
ReadOnly="true"
FullWidth="true"
Adornment="Adornment.End"
AdornmentText="@(layer.ModifiedBy?.Username ?? "")"/>
</MudItem>
</MudGrid>
<MudDivider Class="my-4"/>
<MudTable Items="@records"
Dense="true"
Striped="true"
FixedHeader="true"
FixedFooter="true"
Height="600px">
<HeaderContent>
<MudTh>Code</MudTh>
@foreach (var column in displayedColumns)
{
<MudTh>@column</MudTh>
}
</HeaderContent>
<RowTemplate>
<MudTd DataLabel="Code">@context.Code</MudTd>
@foreach (var column in displayedColumns)
{
<MudTd DataLabel="@column">@GetRecordValue(context, column)</MudTd>
}
</RowTemplate>
<FooterContent>
<MudTd><b>Value1 sum</b></MudTd>
@foreach (var column in displayedColumns)
{
@if (column == "Value1")
{
<MudTd><b>@valueSum.ToString("N2")</b></MudTd>
}
else
{
<MudTd></MudTd>
}
}
</FooterContent>
</MudTable>
}
</MudCardContent>
</MudCard>

View File

@@ -0,0 +1,122 @@
using DiunaBI.Application.DTOModels;
using Microsoft.AspNetCore.Components;
using MudBlazor;
using System.Reflection;
namespace DiunaBI.UI.Shared.Pages;
public partial class LayerDetailPage : ComponentBase
{
[Parameter]
public Guid Id { get; set; }
[Inject]
private ISnackbar Snackbar { get; set; } = null!;
private LayerDto? layer;
private List<RecordDto> records = new();
private List<string> displayedColumns = new();
private double valueSum = 0;
private bool isLoading = false;
protected override async Task OnInitializedAsync()
{
await LoadLayer();
}
protected override async Task OnParametersSetAsync()
{
await LoadLayer();
}
private async Task LoadLayer()
{
isLoading = true;
StateHasChanged();
try
{
layer = await LayerService.GetLayerByIdAsync(Id);
if (layer != null && layer.Records != null)
{
records = layer.Records;
CalculateDisplayedColumns();
CalculateValueSum();
}
}
catch (Exception ex)
{
Console.Error.WriteLine($"Error loading layer: {ex.Message}");
Snackbar.Add("Error loading layer", Severity.Error);
}
finally
{
isLoading = false;
StateHasChanged();
}
}
private void CalculateDisplayedColumns()
{
displayedColumns.Clear();
// Check which Value columns have data
for (int i = 1; i <= 32; i++)
{
var columnName = $"Value{i}";
var hasData = records.Any(r => GetRecordValueByName(r, columnName) != null);
if (hasData)
{
displayedColumns.Add(columnName);
}
}
// Check if Desc1 has data
if (records.Any(r => !string.IsNullOrEmpty(r.Desc1)))
{
displayedColumns.Add("Description1");
}
}
private void CalculateValueSum()
{
valueSum = records
.Where(r => r.Value1.HasValue)
.Sum(r => r.Value1!.Value);
}
private string GetRecordValue(RecordDto record, string columnName)
{
if (columnName == "Description1")
{
return record.Desc1 ?? string.Empty;
}
var value = GetRecordValueByName(record, columnName);
return value.HasValue ? value.Value.ToString("N2") : string.Empty;
}
private double? GetRecordValueByName(RecordDto record, string columnName)
{
var property = typeof(RecordDto).GetProperty(columnName, BindingFlags.Public | BindingFlags.Instance);
if (property != null && property.PropertyType == typeof(double?))
{
return property.GetValue(record) as double?;
}
return null;
}
private void Export()
{
// TODO: Implement export functionality
Snackbar.Add("Export functionality coming soon", Severity.Error);
}
private void ProcessLayer()
{
// TODO: Implement process layer functionality
Snackbar.Add("Process layer functionality coming soon", Severity.Error);
}
}

View File

@@ -0,0 +1,8 @@
@page "/layers"
@using DiunaBI.UI.Shared.Components
<PageTitle>Layers</PageTitle>
<MudContainer MaxWidth="MaxWidth.ExtraExtraLarge">
<LayerListComponent />
</MudContainer>

View File

@@ -0,0 +1,46 @@
@page "/login"
@layout EmptyLayout
<div class="login-page">
<div class="login-container">
<LoginCard />
</div>
</div>
<style>
html, body {
margin: 0 !important;
padding: 0 !important;
height: 100% !important;
overflow: hidden !important;
}
#app {
height: 100% !important;
}
.login-page {
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
margin: 0;
padding: 0;
background-size: cover;
background-position: center;
background-repeat: no-repeat;
background: linear-gradient(rgba(0, 0, 0, 0.3), rgba(0, 0, 0, 0.3)), url('_content/DiunaBI.UI.Shared/images/login-background.jpg') no-repeat center;
background-size: cover;
display: flex;
align-items: center;
justify-content: center;
}
.login-container {
padding: 20px;
width: 100%;
max-width: 450px;
}
</style>

View File

@@ -0,0 +1,172 @@
using System.Net.Http.Json;
using System.Text.Json;
using Microsoft.JSInterop;
namespace DiunaBI.UI.Shared.Services;
public class UserInfo
{
public Guid Id { get; set; }
public string FullName { get; set; } = string.Empty;
public string Email { get; set; } = string.Empty;
public string AvatarUrl { get; set; } = string.Empty;
}
public class AuthService
{
private readonly HttpClient _httpClient;
private readonly IJSRuntime _jsRuntime;
private bool? _isAuthenticated;
private UserInfo? _userInfo = null;
private string? _apiToken;
public event Action<bool>? AuthenticationStateChanged;
public AuthService(HttpClient httpClient, IJSRuntime jsRuntime)
{
_httpClient = httpClient;
_jsRuntime = jsRuntime;
}
public bool IsAuthenticated => _isAuthenticated ?? false;
public UserInfo? CurrentUser => _userInfo;
public async Task<(bool success, string? errorMessage)> ValidateWithBackendAsync(string googleCredential, string fullName, string email, string avatarUrl)
{
try
{
Console.WriteLine($"=== ValidateWithBackend: Sending Google credential for {email} ===");
var response = await _httpClient.PostAsJsonAsync("Auth/apiToken", googleCredential);
if (response.IsSuccessStatusCode)
{
var result = await response.Content.ReadFromJsonAsync<ApiTokenResponse>();
if (result != null)
{
_apiToken = result.Token;
_userInfo = new UserInfo
{
Id = result.Id,
FullName = fullName,
Email = email,
AvatarUrl = avatarUrl
};
await _jsRuntime.InvokeVoidAsync("localStorage.setItem", "api_token", _apiToken);
await _jsRuntime.InvokeVoidAsync("localStorage.setItem", "user_info", JsonSerializer.Serialize(_userInfo));
_httpClient.DefaultRequestHeaders.Authorization =
new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", _apiToken);
_isAuthenticated = true;
Console.WriteLine($"✅ Backend validation successful. UserId={result.Id}");
AuthenticationStateChanged?.Invoke(true);
return (true, null);
}
}
else if (response.StatusCode == System.Net.HttpStatusCode.Unauthorized)
{
Console.WriteLine("❌ User not found in DiunaBI database");
return (false, "User does not exist in DiunaBI database.");
}
else
{
Console.WriteLine($"❌ Backend error: {response.StatusCode}");
return (false, "DiunaBI server error. Please try again.");
}
return (false, "Unexpected error.");
}
catch (HttpRequestException ex)
{
Console.WriteLine($"❌ Network error: {ex.Message}");
return (false, "Cannot connect to DiunaBI server.");
}
catch (Exception ex)
{
Console.WriteLine($"❌ Validation error: {ex.Message}");
return (false, "User verification error.");
}
}
public async Task<bool> CheckAuthenticationAsync()
{
try
{
Console.WriteLine("=== AuthService.CheckAuthenticationAsync START ===");
var token = await _jsRuntime.InvokeAsync<string?>("localStorage.getItem", "api_token");
var userInfoJson = await _jsRuntime.InvokeAsync<string?>("localStorage.getItem", "user_info");
_isAuthenticated = !string.IsNullOrEmpty(token);
if (_isAuthenticated.Value && !string.IsNullOrEmpty(userInfoJson))
{
_apiToken = token;
_userInfo = JsonSerializer.Deserialize<UserInfo>(userInfoJson);
// Restore header
_httpClient.DefaultRequestHeaders.Authorization =
new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", _apiToken);
Console.WriteLine($"✅ Session restored: {_userInfo?.Email}");
}
else
{
Console.WriteLine("❌ No valid session");
}
Console.WriteLine($"=== AuthService.CheckAuthenticationAsync END (authenticated={_isAuthenticated}) ===");
return _isAuthenticated.Value;
}
catch (Exception ex)
{
Console.WriteLine($"❌ CheckAuthentication ERROR: {ex.Message}");
_isAuthenticated = false;
_userInfo = null;
return false;
}
}
public async Task ClearAuthenticationAsync()
{
try
{
Console.WriteLine("=== AuthService.ClearAuthenticationAsync ===");
await _jsRuntime.InvokeVoidAsync("localStorage.removeItem", "api_token");
await _jsRuntime.InvokeVoidAsync("localStorage.removeItem", "user_info");
_apiToken = null;
_isAuthenticated = false;
_userInfo = null;
_httpClient.DefaultRequestHeaders.Authorization = null;
Console.WriteLine("✅ Authentication cleared");
AuthenticationStateChanged?.Invoke(false);
}
catch (Exception ex)
{
Console.WriteLine($"❌ ClearAuthentication ERROR: {ex.Message}");
}
}
public async Task<string?> GetTokenAsync()
{
if (_isAuthenticated != true)
{
await CheckAuthenticationAsync();
}
return _apiToken;
}
private class ApiTokenResponse
{
public string Token { get; set; } = string.Empty;
public Guid Id { get; set; }
public DateTime ExpirationTime { get; set; }
}
}

View File

@@ -0,0 +1,6 @@
namespace DiunaBI.UI.Shared.Services;
public class GoogleAuthConfig
{
public string ClientId { get; set; } = string.Empty;
}

View File

@@ -0,0 +1,13 @@
namespace DiunaBI.UI.Shared.Services;
/// <summary>
/// Platform-agnostic interface for Google authentication
/// </summary>
public interface IGoogleAuthService
{
/// <summary>
/// Initiate Google Sign-In flow
/// </summary>
/// <returns>True if authentication was initiated successfully</returns>
Task<bool> SignInAsync();
}

View File

@@ -0,0 +1,61 @@
using System.Net.Http.Json;
using System.Text.Json;
using DiunaBI.Application.DTOModels;
using DiunaBI.Application.DTOModels.Common;
namespace DiunaBI.UI.Shared.Services;
public class LayerService
{
private readonly HttpClient _httpClient;
public LayerService(HttpClient httpClient)
{
_httpClient = httpClient;
}
private readonly JsonSerializerOptions _jsonOptions = new()
{
PropertyNameCaseInsensitive = true
};
public async Task<PagedResult<LayerDto>> GetLayersAsync(LayerFilterRequest filterRequest)
{
// Calculate start index from page number (page 1 = start 0, page 2 = start 50, etc.)
var start = (filterRequest.Page - 1) * filterRequest.PageSize;
var query = $"Layers?start={start}&limit={filterRequest.PageSize}";
if (!string.IsNullOrEmpty(filterRequest.Search))
query += $"&name={Uri.EscapeDataString(filterRequest.Search)}";
/*
if (type.HasValue)
query += $"&type={type.Value}";
*/
var response = await _httpClient.GetAsync(query);
response.EnsureSuccessStatusCode();
var json = await response.Content.ReadAsStringAsync();
var result = JsonSerializer.Deserialize<PagedResult<LayerDto>>(json, _jsonOptions);
return result ?? new PagedResult<LayerDto>();
}
public async Task<LayerDto?> GetLayerByIdAsync(Guid id)
{
var response = await _httpClient.GetAsync($"Layers/{id}");
if (!response.IsSuccessStatusCode)
return null;
return await response.Content.ReadFromJsonAsync<LayerDto>();
}
public async Task<bool> UpdateRecordsAsync(Guid layerId, List<RecordDto> records)
{
// TODO: Implement if needed - backend doesn't have PUT endpoint yet
// For now we don't need it for read-only view
return await Task.FromResult(false);
}
}

View File

@@ -0,0 +1,30 @@
using Microsoft.JSInterop;
namespace DiunaBI.UI.Shared.Services;
/// <summary>
/// Web implementation of Google authentication using JavaScript SDK
/// </summary>
public class WebGoogleAuthService : IGoogleAuthService
{
private readonly IJSRuntime _jsRuntime;
public WebGoogleAuthService(IJSRuntime jsRuntime)
{
_jsRuntime = jsRuntime;
}
public async Task<bool> SignInAsync()
{
try
{
await _jsRuntime.InvokeVoidAsync("requestGoogleSignIn");
return true;
}
catch (Exception ex)
{
Console.WriteLine($"❌ Web Google Sign-In error: {ex.Message}");
return false;
}
}
}

View File

@@ -0,0 +1,12 @@
@using System.Net.Http
@using System.Net.Http.Json
@using Microsoft.AspNetCore.Components.Forms
@using Microsoft.AspNetCore.Components.Routing
@using Microsoft.AspNetCore.Components.Web
@using static Microsoft.AspNetCore.Components.Web.RenderMode
@using Microsoft.AspNetCore.Components.Web.Virtualization
@using Microsoft.JSInterop
@using DiunaBI.UI.Shared
@using DiunaBI.UI.Shared.Components
@using DiunaBI.Application.DTOModels
@using MudBlazor

Binary file not shown.

After

Width:  |  Height:  |  Size: 965 KiB

View File

@@ -0,0 +1,102 @@
let googleInitialized = false;
window.initGoogleSignIn = function(clientId) {
if (googleInitialized) {
console.log("Google Sign-In already initialized");
return;
}
console.log("🔐 Initializing Google Sign-In (ID Token flow)");
console.log("📋 Received clientId:", clientId);
console.log("📋 ClientId type:", typeof clientId);
console.log("📋 ClientId length:", clientId ? clientId.length : 0);
if (!clientId || clientId === '' || clientId === 'null' || clientId === 'undefined') {
console.error("❌ Invalid clientId received:", clientId);
throw new Error("ClientId is null, empty, or invalid");
}
// Check if Google library is loaded
if (typeof google === 'undefined' || !google.accounts || !google.accounts.id) {
console.error("❌ Google Sign-In library not loaded yet!");
throw new Error("Google Sign-In library not ready");
}
google.accounts.id.initialize({
client_id: clientId,
callback: handleCredentialResponse,
auto_select: false,
cancel_on_tap_outside: true
});
googleInitialized = true;
console.log("✅ Google Sign-In initialized successfully");
};
window.requestGoogleSignIn = function() {
console.log("🚀 Requesting Google Sign-In...");
google.accounts.id.prompt();
};
function handleCredentialResponse(response) {
console.log("=== 🎉 Google Credential Response ===");
try {
if (!response.credential) {
throw new Error("No credential in response");
}
const tokenParts = response.credential.split('.');
console.log("📝 ID Token parts:", tokenParts.length); // Should be 3 (JWT)
console.log("📏 ID Token length:", response.credential.length);
if (tokenParts.length !== 3) {
throw new Error("Invalid JWT format - expected 3 parts (header.payload.signature)");
}
// Dekoduj payload JWT aby wyciągnąć user info
const payload = decodeJwtPayload(response.credential);
const fullName = payload.name || `${payload.given_name || ''} ${payload.family_name || ''}`.trim();
const email = payload.email;
const avatarUrl = payload.picture || '';
console.log("👤 User info from JWT:", { fullName, email });
console.log("📧 Email verified:", payload.email_verified);
// Wywołaj Blazor - przekaż ID token JWT (nie access token!)
DotNet.invokeMethodAsync('DiunaBI.UI.Shared', 'OnGoogleSignInSuccess',
response.credential, // <--- To jest ID token JWT dla backendu
fullName,
email,
avatarUrl)
.then(() => {
console.log("✅ Successfully sent ID token to Blazor");
})
.catch(err => {
console.error("❌ Error calling Blazor:", err);
DotNet.invokeMethodAsync('DiunaBI.UI.Shared', 'OnGoogleSignInError', err.toString());
});
} catch (error) {
console.error("❌ Error processing Google credential:", error);
DotNet.invokeMethodAsync('DiunaBI.UI.Shared', 'OnGoogleSignInError', error.toString());
}
}
function decodeJwtPayload(token) {
try {
const base64Url = token.split('.')[1];
const base64 = base64Url.replace(/-/g, '+').replace(/_/g, '/');
const jsonPayload = decodeURIComponent(
atob(base64)
.split('')
.map(c => '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2))
.join('')
);
return JSON.parse(jsonPayload);
} catch (error) {
console.error("Error decoding JWT:", error);
throw new Error("Invalid JWT format");
}
}