Edit Records

This commit is contained in:
2025-12-01 17:56:17 +01:00
parent 7ea5ed506e
commit c8ded1f0a4
11 changed files with 624 additions and 28 deletions

View File

@@ -17,6 +17,7 @@ public static class ServiceCollectionExtensions
services.AddTransient<HttpLoggingHandler>();
// Configure named HttpClient with logging handler
// Note: Authentication is handled by AuthService setting DefaultRequestHeaders.Authorization
services.AddHttpClient("DiunaBI", client =>
{
client.BaseAddress = new Uri(baseUri);

View File

@@ -0,0 +1,35 @@
using DiunaBI.UI.Shared.Services;
namespace DiunaBI.UI.Shared.Handlers;
public class AuthenticationHandler : DelegatingHandler
{
private readonly TokenProvider _tokenProvider;
public AuthenticationHandler(TokenProvider tokenProvider)
{
_tokenProvider = tokenProvider;
}
protected override async Task<HttpResponseMessage> SendAsync(
HttpRequestMessage request,
CancellationToken cancellationToken)
{
// Get token from TokenProvider
var token = _tokenProvider.Token;
Console.WriteLine($"🔐 AuthenticationHandler: Token = {(string.IsNullOrEmpty(token) ? "NULL" : $"{token[..Math.Min(20, token.Length)]}...")}");
if (!string.IsNullOrEmpty(token))
{
request.Headers.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", token);
Console.WriteLine($"🔐 AuthenticationHandler: Added Bearer token to request");
}
else
{
Console.WriteLine($"🔐 AuthenticationHandler: No token available, request will be unauthorized");
}
return await base.SendAsync(request, cancellationToken);
}
}

View File

@@ -84,12 +84,71 @@
{
<MudTh>@column</MudTh>
}
@if (isEditable)
{
<MudTh>Actions</MudTh>
}
</HeaderContent>
<RowTemplate>
<MudTd DataLabel="Code">@context.Code</MudTd>
@foreach (var column in displayedColumns)
@if (editingRecordId == context.Id)
{
<MudTd DataLabel="@column">@GetRecordValue(context, column)</MudTd>
<MudTd DataLabel="Code">
<MudTextField @bind-Value="editingRecord.Code"
Variant="Variant.Outlined"
Margin="Margin.Dense"
FullWidth="true"/>
</MudTd>
@foreach (var column in displayedColumns)
{
@if (column == "Description1")
{
<MudTd DataLabel="@column">
<MudTextField @bind-Value="editingRecord.Desc1"
Variant="Variant.Outlined"
Margin="Margin.Dense"
FullWidth="true"/>
</MudTd>
}
else
{
<MudTd DataLabel="@column">@GetRecordValue(context, column)</MudTd>
}
}
<MudTd>
<MudIconButton Icon="@Icons.Material.Filled.Check"
Color="Color.Success"
Size="Size.Small"
OnClick="SaveEdit"
Title="Save"/>
<MudIconButton Icon="@Icons.Material.Filled.Close"
Color="Color.Default"
Size="Size.Small"
OnClick="CancelEdit"
Title="Cancel"/>
</MudTd>
}
else
{
<MudTd DataLabel="Code">@context.Code</MudTd>
@foreach (var column in displayedColumns)
{
<MudTd DataLabel="@column">@GetRecordValue(context, column)</MudTd>
}
@if (isEditable)
{
<MudTd>
<MudIconButton Icon="@Icons.Material.Filled.Edit"
Color="Color.Primary"
Size="Size.Small"
OnClick="() => StartEdit(context)"
Title="Edit"/>
<MudIconButton Icon="@Icons.Material.Filled.Delete"
Color="Color.Error"
Size="Size.Small"
OnClick="() => DeleteRecord(context)"
Title="Delete"/>
</MudTd>
}
}
</RowTemplate>
<FooterContent>
@@ -105,8 +164,60 @@
<MudTd></MudTd>
}
}
@if (isEditable)
{
<MudTd></MudTd>
}
</FooterContent>
</MudTable>
@if (isEditable)
{
@if (isAddingNew)
{
<MudPaper Class="mt-4 pa-4" Outlined="true">
<MudGrid>
<MudItem xs="12" md="6">
<MudTextField @bind-Value="newRecord.Code"
Label="Code"
Variant="Variant.Outlined"
FullWidth="true"/>
</MudItem>
<MudItem xs="12" md="6">
<MudTextField @bind-Value="newRecord.Desc1"
Label="Description"
Variant="Variant.Outlined"
FullWidth="true"/>
</MudItem>
<MudItem xs="12" Class="d-flex justify-end">
<MudButton Variant="Variant.Filled"
Color="Color.Success"
OnClick="SaveNewRecord"
StartIcon="@Icons.Material.Filled.Check"
Class="mr-2">
Save
</MudButton>
<MudButton Variant="Variant.Outlined"
Color="Color.Default"
OnClick="CancelAddNew"
StartIcon="@Icons.Material.Filled.Close">
Cancel
</MudButton>
</MudItem>
</MudGrid>
</MudPaper>
}
else
{
<MudButton Variant="Variant.Outlined"
Color="Color.Primary"
OnClick="StartAddNew"
StartIcon="@Icons.Material.Filled.Add"
Class="mt-4">
Add New Record
</MudButton>
}
}
}
</MudCardContent>
</MudCard>

View File

@@ -13,11 +13,19 @@ public partial class LayerDetailPage : ComponentBase
[Inject]
private ISnackbar Snackbar { get; set; } = null!;
[Inject]
private IDialogService DialogService { get; set; } = null!;
private LayerDto? layer;
private List<RecordDto> records = new();
private List<string> displayedColumns = new();
private double valueSum = 0;
private bool isLoading = false;
private Guid? editingRecordId = null;
private RecordDto? editingRecord = null;
private bool isAddingNew = false;
private RecordDto newRecord = new();
private bool isEditable => layer?.Type == LayerType.Dictionary || layer?.Type == LayerType.Administration;
protected override async Task OnInitializedAsync()
{
@@ -119,4 +127,155 @@ public partial class LayerDetailPage : ComponentBase
// TODO: Implement process layer functionality
Snackbar.Add("Process layer functionality coming soon", Severity.Error);
}
// Record editing methods
private void StartEdit(RecordDto record)
{
editingRecordId = record.Id;
editingRecord = new RecordDto
{
Id = record.Id,
Code = record.Code,
Desc1 = record.Desc1,
LayerId = record.LayerId
};
}
private void CancelEdit()
{
editingRecordId = null;
editingRecord = null;
}
private async Task SaveEdit()
{
if (editingRecord == null || layer == null) return;
if (string.IsNullOrWhiteSpace(editingRecord.Code))
{
Snackbar.Add("Code is required", Severity.Warning);
return;
}
if (string.IsNullOrWhiteSpace(editingRecord.Desc1))
{
Snackbar.Add("Description is required", Severity.Warning);
return;
}
try
{
var updated = await LayerService.UpdateRecordAsync(layer.Id, editingRecord.Id, editingRecord);
if (updated != null)
{
var record = records.FirstOrDefault(r => r.Id == editingRecord.Id);
if (record != null)
{
record.Code = updated.Code;
record.Desc1 = updated.Desc1;
record.ModifiedAt = updated.ModifiedAt;
}
editingRecordId = null;
editingRecord = null;
Snackbar.Add("Record updated successfully", Severity.Success);
StateHasChanged();
}
else
{
Snackbar.Add("Failed to update record", Severity.Error);
}
}
catch (Exception ex)
{
Console.Error.WriteLine($"Error updating record: {ex.Message}");
Snackbar.Add("Error updating record", Severity.Error);
}
}
private async Task DeleteRecord(RecordDto record)
{
if (layer == null) return;
var result = await DialogService.ShowMessageBox(
"Confirm Delete",
$"Are you sure you want to delete record '{record.Code}'?",
yesText: "Delete",
cancelText: "Cancel");
if (result == true)
{
try
{
var success = await LayerService.DeleteRecordAsync(layer.Id, record.Id);
if (success)
{
records.Remove(record);
CalculateDisplayedColumns();
CalculateValueSum();
Snackbar.Add("Record deleted successfully", Severity.Success);
}
else
{
Snackbar.Add("Failed to delete record", Severity.Error);
}
}
catch (Exception ex)
{
Console.Error.WriteLine($"Error deleting record: {ex.Message}");
Snackbar.Add("Error deleting record", Severity.Error);
}
}
}
private void StartAddNew()
{
isAddingNew = true;
newRecord = new RecordDto { LayerId = layer?.Id ?? Guid.Empty };
}
private void CancelAddNew()
{
isAddingNew = false;
newRecord = new();
}
private async Task SaveNewRecord()
{
if (layer == null) return;
if (string.IsNullOrWhiteSpace(newRecord.Code))
{
Snackbar.Add("Code is required", Severity.Warning);
return;
}
if (string.IsNullOrWhiteSpace(newRecord.Desc1))
{
Snackbar.Add("Description is required", Severity.Warning);
return;
}
try
{
var created = await LayerService.CreateRecordAsync(layer.Id, newRecord);
if (created != null)
{
records.Add(created);
CalculateDisplayedColumns();
CalculateValueSum();
isAddingNew = false;
newRecord = new();
Snackbar.Add("Record added successfully", Severity.Success);
}
else
{
Snackbar.Add("Failed to add record", Severity.Error);
}
}
catch (Exception ex)
{
Console.Error.WriteLine($"Error adding record: {ex.Message}");
Snackbar.Add("Error adding record", Severity.Error);
}
}
}

View File

@@ -18,9 +18,9 @@ public class AuthService
private bool? _isAuthenticated;
private UserInfo? _userInfo = null;
private string? _apiToken;
public event Action<bool>? AuthenticationStateChanged;
public AuthService(HttpClient httpClient, IJSRuntime jsRuntime)
{
_httpClient = httpClient;
@@ -51,17 +51,17 @@ public class AuthService
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 =
_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);
}
}
@@ -109,7 +109,7 @@ public class AuthService
// Restore header
_httpClient.DefaultRequestHeaders.Authorization =
new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", _apiToken);
Console.WriteLine($"✅ Session restored: {_userInfo?.Email}");
}
else
@@ -137,11 +137,11 @@ public class AuthService
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");

View File

@@ -56,4 +56,38 @@ public class LayerService
// For now we don't need it for read-only view
return await Task.FromResult(false);
}
public async Task<RecordDto?> CreateRecordAsync(Guid layerId, RecordDto record)
{
var response = await _httpClient.PostAsJsonAsync($"Layers/{layerId}/records", record);
if (!response.IsSuccessStatusCode)
{
var error = await response.Content.ReadAsStringAsync();
Console.Error.WriteLine($"CreateRecordAsync failed: {response.StatusCode} - {error}");
return null;
}
return await response.Content.ReadFromJsonAsync<RecordDto>();
}
public async Task<RecordDto?> UpdateRecordAsync(Guid layerId, Guid recordId, RecordDto record)
{
var response = await _httpClient.PutAsJsonAsync($"Layers/{layerId}/records/{recordId}", record);
if (!response.IsSuccessStatusCode)
{
var error = await response.Content.ReadAsStringAsync();
Console.Error.WriteLine($"UpdateRecordAsync failed: {response.StatusCode} - {error}");
return null;
}
return await response.Content.ReadFromJsonAsync<RecordDto>();
}
public async Task<bool> DeleteRecordAsync(Guid layerId, Guid recordId)
{
var response = await _httpClient.DeleteAsync($"Layers/{layerId}/records/{recordId}");
return response.IsSuccessStatusCode;
}
}

View File

@@ -0,0 +1,6 @@
namespace DiunaBI.UI.Shared.Services;
public class TokenProvider
{
public string? Token { get; set; }
}