Edit Records
This commit is contained in:
@@ -1,9 +1,11 @@
|
|||||||
using DiunaBI.API.Services;
|
using DiunaBI.API.Services;
|
||||||
using DiunaBI.Domain.Entities;
|
using DiunaBI.Domain.Entities;
|
||||||
|
using Microsoft.AspNetCore.Authorization;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
|
||||||
namespace DiunaBI.API.Controllers;
|
namespace DiunaBI.API.Controllers;
|
||||||
|
|
||||||
|
[AllowAnonymous]
|
||||||
[ApiController]
|
[ApiController]
|
||||||
[Route("[controller]")]
|
[Route("[controller]")]
|
||||||
public class AuthController(
|
public class AuthController(
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ using DiunaBI.Application.DTOModels.Common;
|
|||||||
|
|
||||||
namespace DiunaBI.API.Controllers;
|
namespace DiunaBI.API.Controllers;
|
||||||
|
|
||||||
|
[Authorize]
|
||||||
[ApiController]
|
[ApiController]
|
||||||
[Route("[controller]")]
|
[Route("[controller]")]
|
||||||
public class DataInboxController : Controller
|
public class DataInboxController : Controller
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ using DiunaBI.Infrastructure.Services;
|
|||||||
|
|
||||||
namespace DiunaBI.API.Controllers;
|
namespace DiunaBI.API.Controllers;
|
||||||
|
|
||||||
|
[Authorize]
|
||||||
[ApiController]
|
[ApiController]
|
||||||
[Route("[controller]")]
|
[Route("[controller]")]
|
||||||
public class LayersController : Controller
|
public class LayersController : Controller
|
||||||
@@ -727,4 +728,209 @@ public class LayersController : Controller
|
|||||||
throw;
|
throw;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Record CRUD operations
|
||||||
|
[HttpPost]
|
||||||
|
[Route("{layerId:guid}/records")]
|
||||||
|
public IActionResult CreateRecord(Guid layerId, [FromBody] RecordDto recordDto)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var userId = Request.Headers["UserId"].ToString();
|
||||||
|
if (string.IsNullOrEmpty(userId))
|
||||||
|
{
|
||||||
|
_logger.LogWarning("CreateRecord: No UserId in request headers");
|
||||||
|
return Unauthorized();
|
||||||
|
}
|
||||||
|
|
||||||
|
var layer = _db.Layers.FirstOrDefault(x => x.Id == layerId && !x.IsDeleted);
|
||||||
|
if (layer == null)
|
||||||
|
{
|
||||||
|
_logger.LogWarning("CreateRecord: Layer {LayerId} not found", layerId);
|
||||||
|
return NotFound("Layer not found");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (layer.Type != Domain.Entities.LayerType.Dictionary && layer.Type != Domain.Entities.LayerType.Administration)
|
||||||
|
{
|
||||||
|
_logger.LogWarning("CreateRecord: Layer {LayerId} is not editable (type: {LayerType})", layerId, layer.Type);
|
||||||
|
return BadRequest("Only Dictionary and Administration layers can be edited");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(recordDto.Code))
|
||||||
|
{
|
||||||
|
return BadRequest("Code is required");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(recordDto.Desc1))
|
||||||
|
{
|
||||||
|
return BadRequest("Desc1 is required");
|
||||||
|
}
|
||||||
|
|
||||||
|
var record = new Record
|
||||||
|
{
|
||||||
|
Id = Guid.NewGuid(),
|
||||||
|
Code = recordDto.Code,
|
||||||
|
Desc1 = recordDto.Desc1,
|
||||||
|
LayerId = layerId,
|
||||||
|
CreatedAt = DateTime.UtcNow,
|
||||||
|
ModifiedAt = DateTime.UtcNow,
|
||||||
|
CreatedById = Guid.Parse(userId),
|
||||||
|
ModifiedById = Guid.Parse(userId),
|
||||||
|
IsDeleted = false
|
||||||
|
};
|
||||||
|
|
||||||
|
_db.Records.Add(record);
|
||||||
|
|
||||||
|
// Update layer modified info
|
||||||
|
layer.ModifiedAt = DateTime.UtcNow;
|
||||||
|
layer.ModifiedById = Guid.Parse(userId);
|
||||||
|
|
||||||
|
_db.SaveChanges();
|
||||||
|
|
||||||
|
_logger.LogInformation("CreateRecord: Created record {RecordId} in layer {LayerId}", record.Id, layerId);
|
||||||
|
|
||||||
|
return Ok(new RecordDto
|
||||||
|
{
|
||||||
|
Id = record.Id,
|
||||||
|
Code = record.Code,
|
||||||
|
Desc1 = record.Desc1,
|
||||||
|
LayerId = record.LayerId,
|
||||||
|
CreatedAt = record.CreatedAt,
|
||||||
|
ModifiedAt = record.ModifiedAt,
|
||||||
|
CreatedById = record.CreatedById,
|
||||||
|
ModifiedById = record.ModifiedById
|
||||||
|
});
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
_logger.LogError(e, "CreateRecord: Error creating record in layer {LayerId}", layerId);
|
||||||
|
return BadRequest(e.ToString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPut]
|
||||||
|
[Route("{layerId:guid}/records/{recordId:guid}")]
|
||||||
|
public IActionResult UpdateRecord(Guid layerId, Guid recordId, [FromBody] RecordDto recordDto)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var userId = Request.Headers["UserId"].ToString();
|
||||||
|
if (string.IsNullOrEmpty(userId))
|
||||||
|
{
|
||||||
|
_logger.LogWarning("UpdateRecord: No UserId in request headers");
|
||||||
|
return Unauthorized();
|
||||||
|
}
|
||||||
|
|
||||||
|
var layer = _db.Layers.FirstOrDefault(x => x.Id == layerId && !x.IsDeleted);
|
||||||
|
if (layer == null)
|
||||||
|
{
|
||||||
|
_logger.LogWarning("UpdateRecord: Layer {LayerId} not found", layerId);
|
||||||
|
return NotFound("Layer not found");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (layer.Type != Domain.Entities.LayerType.Dictionary && layer.Type != Domain.Entities.LayerType.Administration)
|
||||||
|
{
|
||||||
|
_logger.LogWarning("UpdateRecord: Layer {LayerId} is not editable (type: {LayerType})", layerId, layer.Type);
|
||||||
|
return BadRequest("Only Dictionary and Administration layers can be edited");
|
||||||
|
}
|
||||||
|
|
||||||
|
var record = _db.Records.FirstOrDefault(x => x.Id == recordId && x.LayerId == layerId);
|
||||||
|
if (record == null)
|
||||||
|
{
|
||||||
|
_logger.LogWarning("UpdateRecord: Record {RecordId} not found in layer {LayerId}", recordId, layerId);
|
||||||
|
return NotFound("Record not found");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(recordDto.Code))
|
||||||
|
{
|
||||||
|
return BadRequest("Code is required");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(recordDto.Desc1))
|
||||||
|
{
|
||||||
|
return BadRequest("Desc1 is required");
|
||||||
|
}
|
||||||
|
|
||||||
|
record.Desc1 = recordDto.Desc1;
|
||||||
|
record.ModifiedAt = DateTime.UtcNow;
|
||||||
|
record.ModifiedById = Guid.Parse(userId);
|
||||||
|
|
||||||
|
// Update layer modified info
|
||||||
|
layer.ModifiedAt = DateTime.UtcNow;
|
||||||
|
layer.ModifiedById = Guid.Parse(userId);
|
||||||
|
|
||||||
|
_db.SaveChanges();
|
||||||
|
|
||||||
|
_logger.LogInformation("UpdateRecord: Updated record {RecordId} in layer {LayerId}", recordId, layerId);
|
||||||
|
|
||||||
|
return Ok(new RecordDto
|
||||||
|
{
|
||||||
|
Id = record.Id,
|
||||||
|
Code = record.Code,
|
||||||
|
Desc1 = record.Desc1,
|
||||||
|
LayerId = record.LayerId,
|
||||||
|
CreatedAt = record.CreatedAt,
|
||||||
|
ModifiedAt = record.ModifiedAt,
|
||||||
|
CreatedById = record.CreatedById,
|
||||||
|
ModifiedById = record.ModifiedById
|
||||||
|
});
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
_logger.LogError(e, "UpdateRecord: Error updating record {RecordId} in layer {LayerId}", recordId, layerId);
|
||||||
|
return BadRequest(e.ToString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpDelete]
|
||||||
|
[Route("{layerId:guid}/records/{recordId:guid}")]
|
||||||
|
public IActionResult DeleteRecord(Guid layerId, Guid recordId)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var userId = Request.Headers["UserId"].ToString();
|
||||||
|
if (string.IsNullOrEmpty(userId))
|
||||||
|
{
|
||||||
|
_logger.LogWarning("DeleteRecord: No UserId in request headers");
|
||||||
|
return Unauthorized();
|
||||||
|
}
|
||||||
|
|
||||||
|
var layer = _db.Layers.FirstOrDefault(x => x.Id == layerId && !x.IsDeleted);
|
||||||
|
if (layer == null)
|
||||||
|
{
|
||||||
|
_logger.LogWarning("DeleteRecord: Layer {LayerId} not found", layerId);
|
||||||
|
return NotFound("Layer not found");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (layer.Type != Domain.Entities.LayerType.Dictionary && layer.Type != Domain.Entities.LayerType.Administration)
|
||||||
|
{
|
||||||
|
_logger.LogWarning("DeleteRecord: Layer {LayerId} is not editable (type: {LayerType})", layerId, layer.Type);
|
||||||
|
return BadRequest("Only Dictionary and Administration layers can be edited");
|
||||||
|
}
|
||||||
|
|
||||||
|
var record = _db.Records.FirstOrDefault(x => x.Id == recordId && x.LayerId == layerId);
|
||||||
|
if (record == null)
|
||||||
|
{
|
||||||
|
_logger.LogWarning("DeleteRecord: Record {RecordId} not found in layer {LayerId}", recordId, layerId);
|
||||||
|
return NotFound("Record not found");
|
||||||
|
}
|
||||||
|
|
||||||
|
_db.Records.Remove(record);
|
||||||
|
|
||||||
|
// Update layer modified info
|
||||||
|
layer.ModifiedAt = DateTime.UtcNow;
|
||||||
|
layer.ModifiedById = Guid.Parse(userId);
|
||||||
|
|
||||||
|
_db.SaveChanges();
|
||||||
|
|
||||||
|
_logger.LogInformation("DeleteRecord: Deleted record {RecordId} from layer {LayerId}", recordId, layerId);
|
||||||
|
|
||||||
|
return Ok();
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
_logger.LogError(e, "DeleteRecord: Error deleting record {RecordId} from layer {LayerId}", recordId, layerId);
|
||||||
|
return BadRequest(e.ToString());
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -177,26 +177,67 @@ else
|
|||||||
|
|
||||||
pluginManager.LoadPluginsFromDirectory(pluginsPath);
|
pluginManager.LoadPluginsFromDirectory(pluginsPath);
|
||||||
|
|
||||||
app.Use(async (context, next) =>
|
|
||||||
{
|
|
||||||
var token = context.Request.Headers.Authorization.ToString();
|
|
||||||
if (token.Length > 0
|
|
||||||
&& !context.Request.Path.ToString().Contains("getForPowerBI")
|
|
||||||
&& !context.Request.Path.ToString().Contains("getConfiguration")
|
|
||||||
&& !context.Request.Path.ToString().Contains("DataInbox/Add"))
|
|
||||||
{
|
|
||||||
var handler = new JwtSecurityTokenHandler();
|
|
||||||
var data = handler.ReadJwtToken(token.Split(' ')[1]);
|
|
||||||
context.Request.Headers.Append("UserId", new Microsoft.Extensions.Primitives.StringValues(data.Subject));
|
|
||||||
}
|
|
||||||
await next(context);
|
|
||||||
});
|
|
||||||
|
|
||||||
app.UseCors("CORSPolicy");
|
app.UseCors("CORSPolicy");
|
||||||
|
|
||||||
app.UseAuthentication();
|
app.UseAuthentication();
|
||||||
app.UseAuthorization();
|
app.UseAuthorization();
|
||||||
|
|
||||||
|
// Middleware to extract UserId from JWT token AFTER authentication
|
||||||
|
// This must run after UseAuthentication() so the JWT is already validated
|
||||||
|
app.Use(async (context, next) =>
|
||||||
|
{
|
||||||
|
var logger = context.RequestServices.GetRequiredService<ILogger<Program>>();
|
||||||
|
logger.LogInformation("🔍 UserId Extraction Middleware - Path: {Path}, Method: {Method}",
|
||||||
|
context.Request.Path, context.Request.Method);
|
||||||
|
|
||||||
|
var token = context.Request.Headers.Authorization.ToString();
|
||||||
|
logger.LogInformation("🔍 Authorization header: {Token}",
|
||||||
|
string.IsNullOrEmpty(token) ? "NULL/EMPTY" : $"{token[..Math.Min(30, token.Length)]}...");
|
||||||
|
|
||||||
|
if (!string.IsNullOrEmpty(token) && token.StartsWith("Bearer ", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var handler = new JwtSecurityTokenHandler();
|
||||||
|
var jwtToken = handler.ReadJwtToken(token.Split(' ')[1]);
|
||||||
|
|
||||||
|
// Try to get UserId from Subject claim first, then fall back to NameIdentifier
|
||||||
|
var userId = jwtToken.Subject;
|
||||||
|
if (string.IsNullOrEmpty(userId))
|
||||||
|
{
|
||||||
|
// Try NameIdentifier claim (ClaimTypes.NameIdentifier)
|
||||||
|
var nameIdClaim = jwtToken.Claims.FirstOrDefault(c =>
|
||||||
|
c.Type == "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier" ||
|
||||||
|
c.Type == "nameid");
|
||||||
|
userId = nameIdClaim?.Value;
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.LogInformation("🔍 JWT UserId: {UserId}", userId ?? "NULL");
|
||||||
|
|
||||||
|
if (!string.IsNullOrEmpty(userId))
|
||||||
|
{
|
||||||
|
// Use indexer to set/replace header value instead of Append
|
||||||
|
context.Request.Headers["UserId"] = userId;
|
||||||
|
logger.LogInformation("✅ Set UserId header to: {UserId}", userId);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
logger.LogWarning("❌ UserId not found in JWT claims");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
logger.LogError(ex, "❌ Failed to extract UserId from JWT token");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
logger.LogWarning("❌ No valid Bearer token found");
|
||||||
|
}
|
||||||
|
|
||||||
|
await next(context);
|
||||||
|
});
|
||||||
|
|
||||||
app.MapControllers();
|
app.MapControllers();
|
||||||
|
|
||||||
app.MapGet("/health", () => Results.Ok(new { status = "OK", timestamp = DateTime.UtcNow }))
|
app.MapGet("/health", () => Results.Ok(new { status = "OK", timestamp = DateTime.UtcNow }))
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ public static class ServiceCollectionExtensions
|
|||||||
services.AddTransient<HttpLoggingHandler>();
|
services.AddTransient<HttpLoggingHandler>();
|
||||||
|
|
||||||
// Configure named HttpClient with logging handler
|
// Configure named HttpClient with logging handler
|
||||||
|
// Note: Authentication is handled by AuthService setting DefaultRequestHeaders.Authorization
|
||||||
services.AddHttpClient("DiunaBI", client =>
|
services.AddHttpClient("DiunaBI", client =>
|
||||||
{
|
{
|
||||||
client.BaseAddress = new Uri(baseUri);
|
client.BaseAddress = new Uri(baseUri);
|
||||||
|
|||||||
35
DiunaBI.UI.Shared/Handlers/AuthenticationHandler.cs
Normal file
35
DiunaBI.UI.Shared/Handlers/AuthenticationHandler.cs
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -84,12 +84,71 @@
|
|||||||
{
|
{
|
||||||
<MudTh>@column</MudTh>
|
<MudTh>@column</MudTh>
|
||||||
}
|
}
|
||||||
|
@if (isEditable)
|
||||||
|
{
|
||||||
|
<MudTh>Actions</MudTh>
|
||||||
|
}
|
||||||
</HeaderContent>
|
</HeaderContent>
|
||||||
<RowTemplate>
|
<RowTemplate>
|
||||||
<MudTd DataLabel="Code">@context.Code</MudTd>
|
@if (editingRecordId == context.Id)
|
||||||
@foreach (var column in displayedColumns)
|
|
||||||
{
|
{
|
||||||
<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>
|
</RowTemplate>
|
||||||
<FooterContent>
|
<FooterContent>
|
||||||
@@ -105,8 +164,60 @@
|
|||||||
<MudTd></MudTd>
|
<MudTd></MudTd>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@if (isEditable)
|
||||||
|
{
|
||||||
|
<MudTd></MudTd>
|
||||||
|
}
|
||||||
</FooterContent>
|
</FooterContent>
|
||||||
</MudTable>
|
</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>
|
</MudCardContent>
|
||||||
</MudCard>
|
</MudCard>
|
||||||
|
|||||||
@@ -13,11 +13,19 @@ public partial class LayerDetailPage : ComponentBase
|
|||||||
[Inject]
|
[Inject]
|
||||||
private ISnackbar Snackbar { get; set; } = null!;
|
private ISnackbar Snackbar { get; set; } = null!;
|
||||||
|
|
||||||
|
[Inject]
|
||||||
|
private IDialogService DialogService { get; set; } = null!;
|
||||||
|
|
||||||
private LayerDto? layer;
|
private LayerDto? layer;
|
||||||
private List<RecordDto> records = new();
|
private List<RecordDto> records = new();
|
||||||
private List<string> displayedColumns = new();
|
private List<string> displayedColumns = new();
|
||||||
private double valueSum = 0;
|
private double valueSum = 0;
|
||||||
private bool isLoading = false;
|
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()
|
protected override async Task OnInitializedAsync()
|
||||||
{
|
{
|
||||||
@@ -119,4 +127,155 @@ public partial class LayerDetailPage : ComponentBase
|
|||||||
// TODO: Implement process layer functionality
|
// TODO: Implement process layer functionality
|
||||||
Snackbar.Add("Process layer functionality coming soon", Severity.Error);
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,9 +18,9 @@ public class AuthService
|
|||||||
private bool? _isAuthenticated;
|
private bool? _isAuthenticated;
|
||||||
private UserInfo? _userInfo = null;
|
private UserInfo? _userInfo = null;
|
||||||
private string? _apiToken;
|
private string? _apiToken;
|
||||||
|
|
||||||
public event Action<bool>? AuthenticationStateChanged;
|
public event Action<bool>? AuthenticationStateChanged;
|
||||||
|
|
||||||
public AuthService(HttpClient httpClient, IJSRuntime jsRuntime)
|
public AuthService(HttpClient httpClient, IJSRuntime jsRuntime)
|
||||||
{
|
{
|
||||||
_httpClient = httpClient;
|
_httpClient = httpClient;
|
||||||
@@ -51,17 +51,17 @@ public class AuthService
|
|||||||
Email = email,
|
Email = email,
|
||||||
AvatarUrl = avatarUrl
|
AvatarUrl = avatarUrl
|
||||||
};
|
};
|
||||||
|
|
||||||
await _jsRuntime.InvokeVoidAsync("localStorage.setItem", "api_token", _apiToken);
|
await _jsRuntime.InvokeVoidAsync("localStorage.setItem", "api_token", _apiToken);
|
||||||
await _jsRuntime.InvokeVoidAsync("localStorage.setItem", "user_info", JsonSerializer.Serialize(_userInfo));
|
await _jsRuntime.InvokeVoidAsync("localStorage.setItem", "user_info", JsonSerializer.Serialize(_userInfo));
|
||||||
|
|
||||||
_httpClient.DefaultRequestHeaders.Authorization =
|
_httpClient.DefaultRequestHeaders.Authorization =
|
||||||
new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", _apiToken);
|
new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", _apiToken);
|
||||||
|
|
||||||
_isAuthenticated = true;
|
_isAuthenticated = true;
|
||||||
Console.WriteLine($"✅ Backend validation successful. UserId={result.Id}");
|
Console.WriteLine($"✅ Backend validation successful. UserId={result.Id}");
|
||||||
AuthenticationStateChanged?.Invoke(true);
|
AuthenticationStateChanged?.Invoke(true);
|
||||||
|
|
||||||
return (true, null);
|
return (true, null);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -109,7 +109,7 @@ public class AuthService
|
|||||||
// Restore header
|
// Restore header
|
||||||
_httpClient.DefaultRequestHeaders.Authorization =
|
_httpClient.DefaultRequestHeaders.Authorization =
|
||||||
new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", _apiToken);
|
new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", _apiToken);
|
||||||
|
|
||||||
Console.WriteLine($"✅ Session restored: {_userInfo?.Email}");
|
Console.WriteLine($"✅ Session restored: {_userInfo?.Email}");
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
@@ -137,11 +137,11 @@ public class AuthService
|
|||||||
Console.WriteLine("=== AuthService.ClearAuthenticationAsync ===");
|
Console.WriteLine("=== AuthService.ClearAuthenticationAsync ===");
|
||||||
await _jsRuntime.InvokeVoidAsync("localStorage.removeItem", "api_token");
|
await _jsRuntime.InvokeVoidAsync("localStorage.removeItem", "api_token");
|
||||||
await _jsRuntime.InvokeVoidAsync("localStorage.removeItem", "user_info");
|
await _jsRuntime.InvokeVoidAsync("localStorage.removeItem", "user_info");
|
||||||
|
|
||||||
_apiToken = null;
|
_apiToken = null;
|
||||||
_isAuthenticated = false;
|
_isAuthenticated = false;
|
||||||
_userInfo = null;
|
_userInfo = null;
|
||||||
|
|
||||||
_httpClient.DefaultRequestHeaders.Authorization = null;
|
_httpClient.DefaultRequestHeaders.Authorization = null;
|
||||||
|
|
||||||
Console.WriteLine("✅ Authentication cleared");
|
Console.WriteLine("✅ Authentication cleared");
|
||||||
|
|||||||
@@ -56,4 +56,38 @@ public class LayerService
|
|||||||
// For now we don't need it for read-only view
|
// For now we don't need it for read-only view
|
||||||
return await Task.FromResult(false);
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
6
DiunaBI.UI.Shared/Services/TokenProvider.cs
Normal file
6
DiunaBI.UI.Shared/Services/TokenProvider.cs
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
namespace DiunaBI.UI.Shared.Services;
|
||||||
|
|
||||||
|
public class TokenProvider
|
||||||
|
{
|
||||||
|
public string? Token { get; set; }
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user