2025-05-31 19:26:02 +02:00
|
|
|
using System.Globalization;
|
|
|
|
|
using System.Text;
|
2025-12-01 18:37:09 +01:00
|
|
|
using System.Text.Json;
|
2025-05-31 19:26:02 +02:00
|
|
|
using Google.Apis.Sheets.v4;
|
|
|
|
|
using Microsoft.AspNetCore.Authorization;
|
|
|
|
|
using Microsoft.AspNetCore.Mvc;
|
|
|
|
|
using Microsoft.EntityFrameworkCore;
|
2025-11-19 16:19:06 +01:00
|
|
|
using DiunaBI.Application.DTOModels;
|
|
|
|
|
using DiunaBI.Application.DTOModels.Common;
|
2025-11-05 20:50:25 +01:00
|
|
|
using DiunaBI.Domain.Entities;
|
|
|
|
|
using DiunaBI.Infrastructure.Data;
|
|
|
|
|
using DiunaBI.Infrastructure.Services;
|
2025-05-31 19:26:02 +02:00
|
|
|
|
2025-11-05 20:50:25 +01:00
|
|
|
namespace DiunaBI.API.Controllers;
|
2025-05-31 19:26:02 +02:00
|
|
|
|
2025-12-01 17:56:17 +01:00
|
|
|
[Authorize]
|
2025-05-31 19:26:02 +02:00
|
|
|
[ApiController]
|
2025-11-13 11:15:32 +01:00
|
|
|
[Route("[controller]")]
|
2025-05-31 19:26:02 +02:00
|
|
|
public class LayersController : Controller
|
|
|
|
|
{
|
|
|
|
|
private readonly AppDbContext _db;
|
|
|
|
|
private readonly SpreadsheetsResource.ValuesResource? _googleSheetValues;
|
|
|
|
|
private readonly GoogleDriveHelper _googleDriveHelper;
|
|
|
|
|
private readonly IConfiguration _configuration;
|
2025-06-02 16:54:33 +02:00
|
|
|
private readonly PluginManager _pluginManager;
|
2025-06-06 20:23:36 +02:00
|
|
|
private readonly ILogger<LayersController> _logger;
|
2025-05-31 19:26:02 +02:00
|
|
|
|
|
|
|
|
public LayersController(
|
|
|
|
|
AppDbContext db,
|
2025-06-06 20:23:36 +02:00
|
|
|
SpreadsheetsResource.ValuesResource? googleSheetValues,
|
2025-05-31 19:26:02 +02:00
|
|
|
GoogleDriveHelper googleDriveHelper,
|
|
|
|
|
IConfiguration configuration,
|
2025-06-02 18:53:25 +02:00
|
|
|
PluginManager pluginManager,
|
2025-06-08 11:08:48 +02:00
|
|
|
ILogger<LayersController> logger
|
2025-05-31 19:26:02 +02:00
|
|
|
)
|
|
|
|
|
{
|
|
|
|
|
_db = db;
|
2025-06-02 18:53:25 +02:00
|
|
|
_googleSheetValues = googleSheetValues;
|
2025-05-31 19:26:02 +02:00
|
|
|
_googleDriveHelper = googleDriveHelper;
|
|
|
|
|
_configuration = configuration;
|
2025-06-02 16:54:33 +02:00
|
|
|
_pluginManager = pluginManager;
|
2025-06-02 18:53:25 +02:00
|
|
|
_logger = logger;
|
2025-05-31 19:26:02 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
[HttpGet]
|
2025-11-19 16:19:06 +01:00
|
|
|
[Route("")]
|
|
|
|
|
public IActionResult GetAll([FromQuery] int start, [FromQuery] int limit, [FromQuery] string? name, [FromQuery] Domain.Entities.LayerType? type)
|
2025-05-31 19:26:02 +02:00
|
|
|
{
|
|
|
|
|
try
|
|
|
|
|
{
|
2025-12-05 23:41:56 +01:00
|
|
|
// Validate pagination parameters
|
|
|
|
|
if (limit <= 0 || limit > 1000)
|
|
|
|
|
{
|
|
|
|
|
return BadRequest("Limit must be between 1 and 1000");
|
|
|
|
|
}
|
|
|
|
|
if (start < 0)
|
|
|
|
|
{
|
|
|
|
|
return BadRequest("Start must be non-negative");
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-19 16:19:06 +01:00
|
|
|
var query = _db.Layers.Where(x => !x.IsDeleted);
|
|
|
|
|
|
2025-05-31 19:26:02 +02:00
|
|
|
if (name != null)
|
|
|
|
|
{
|
2025-11-19 16:19:06 +01:00
|
|
|
query = query.Where(x => x.Name != null && x.Name.Contains(name));
|
2025-05-31 19:26:02 +02:00
|
|
|
}
|
|
|
|
|
|
2025-11-19 16:19:06 +01:00
|
|
|
if (type.HasValue)
|
2025-05-31 19:26:02 +02:00
|
|
|
{
|
2025-11-19 16:19:06 +01:00
|
|
|
query = query.Where(x => x.Type == type.Value);
|
2025-05-31 19:26:02 +02:00
|
|
|
}
|
|
|
|
|
|
2025-11-19 16:19:06 +01:00
|
|
|
var totalCount = query.Count();
|
|
|
|
|
|
|
|
|
|
var items = query
|
2025-05-31 19:26:02 +02:00
|
|
|
.OrderByDescending(x => x.Number)
|
2025-11-19 16:19:06 +01:00
|
|
|
.Skip(start)
|
|
|
|
|
.Take(limit)
|
|
|
|
|
.AsNoTracking()
|
|
|
|
|
.Select(x => new LayerDto
|
|
|
|
|
{
|
|
|
|
|
Id = x.Id,
|
|
|
|
|
Number = x.Number,
|
|
|
|
|
Name = x.Name,
|
|
|
|
|
Type = (Application.DTOModels.LayerType)x.Type,
|
|
|
|
|
CreatedAt = x.CreatedAt,
|
|
|
|
|
ModifiedAt = x.ModifiedAt,
|
|
|
|
|
CreatedById = x.CreatedById,
|
|
|
|
|
ModifiedById = x.ModifiedById,
|
|
|
|
|
IsDeleted = x.IsDeleted,
|
|
|
|
|
IsCancelled = x.IsCancelled,
|
|
|
|
|
ParentId = x.ParentId
|
|
|
|
|
})
|
|
|
|
|
.ToList();
|
|
|
|
|
|
|
|
|
|
var pagedResult = new PagedResult<LayerDto>
|
|
|
|
|
{
|
|
|
|
|
Items = items,
|
|
|
|
|
TotalCount = totalCount,
|
|
|
|
|
Page = (start / limit) + 1,
|
|
|
|
|
PageSize = limit
|
|
|
|
|
};
|
2025-06-02 18:53:25 +02:00
|
|
|
|
2025-11-19 16:19:06 +01:00
|
|
|
_logger.LogDebug("GetAll: Retrieved {Count} of {TotalCount} layers (page {Page}) with filter name={Name}, type={Type}",
|
|
|
|
|
items.Count, totalCount, pagedResult.Page, name, type);
|
2025-06-02 18:53:25 +02:00
|
|
|
|
2025-11-19 16:19:06 +01:00
|
|
|
return Ok(pagedResult);
|
2025-05-31 19:26:02 +02:00
|
|
|
}
|
|
|
|
|
catch (Exception e)
|
|
|
|
|
{
|
2025-06-02 18:53:25 +02:00
|
|
|
_logger.LogError(e, "GetAll: Error retrieving layers");
|
2025-12-05 21:37:15 +01:00
|
|
|
return BadRequest("An error occurred processing your request");
|
2025-05-31 19:26:02 +02:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
[HttpGet]
|
|
|
|
|
[Route("{id:guid}")]
|
|
|
|
|
public IActionResult Get(Guid id)
|
|
|
|
|
{
|
|
|
|
|
try
|
|
|
|
|
{
|
2025-06-02 18:53:25 +02:00
|
|
|
var layer = _db.Layers
|
2025-05-31 19:26:02 +02:00
|
|
|
.Include(x => x.CreatedBy)
|
|
|
|
|
.Include(x => x.ModifiedBy)
|
2025-06-02 18:53:25 +02:00
|
|
|
.Include(x => x.Records).AsNoTracking().First(x => x.Id == id && !x.IsDeleted);
|
|
|
|
|
|
|
|
|
|
_logger.LogDebug("Get: Retrieved layer {LayerId} {LayerName}", id, layer.Name);
|
|
|
|
|
return Ok(layer);
|
2025-05-31 19:26:02 +02:00
|
|
|
}
|
|
|
|
|
catch (Exception e)
|
|
|
|
|
{
|
2025-06-02 18:53:25 +02:00
|
|
|
_logger.LogError(e, "Get: Error retrieving layer {LayerId}", id);
|
2025-12-05 21:37:15 +01:00
|
|
|
return BadRequest("An error occurred processing your request");
|
2025-05-31 19:26:02 +02:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
[HttpGet]
|
|
|
|
|
[Route("getConfiguration/{apiKey}/{number:int}")]
|
2025-11-26 14:14:56 +01:00
|
|
|
[AllowAnonymous]
|
2025-05-31 19:26:02 +02:00
|
|
|
public IActionResult GetConfigurationByNumber(string apiKey, int number)
|
|
|
|
|
{
|
|
|
|
|
if (apiKey != _configuration["apiKey"])
|
|
|
|
|
{
|
2025-06-02 18:53:25 +02:00
|
|
|
_logger.LogWarning("Configuration: Unauthorized request - wrong apiKey for layer {LayerNumber}", number);
|
2025-05-31 19:26:02 +02:00
|
|
|
return Unauthorized();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
try
|
|
|
|
|
{
|
2025-06-02 18:53:25 +02:00
|
|
|
if (!Request.Headers.TryGetValue("Authorization", out var authHeader))
|
2025-05-31 19:26:02 +02:00
|
|
|
{
|
2025-06-02 18:53:25 +02:00
|
|
|
_logger.LogWarning("Configuration: Unauthorized request - no authorization header for layer {LayerNumber}", number);
|
2025-05-31 19:26:02 +02:00
|
|
|
return Unauthorized();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var credentialsArr = authHeader.ToString().Split(" ");
|
|
|
|
|
if (credentialsArr.Length != 2)
|
|
|
|
|
{
|
2025-06-02 18:53:25 +02:00
|
|
|
_logger.LogWarning("Configuration: Unauthorized request - wrong auth header format for layer {LayerNumber}", number);
|
2025-05-31 19:26:02 +02:00
|
|
|
return Unauthorized();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var authValue = Encoding.UTF8.GetString(Convert.FromBase64String(credentialsArr[1]));
|
|
|
|
|
var username = authValue.Split(':')[0];
|
|
|
|
|
var password = authValue.Split(':')[1];
|
2025-11-26 14:34:32 +01:00
|
|
|
if (username != _configuration["apiUser"] || password != _configuration["apiPass"])
|
2025-05-31 19:26:02 +02:00
|
|
|
{
|
2025-06-02 18:53:25 +02:00
|
|
|
_logger.LogWarning("Configuration: Unauthorized request - bad credentials for layer {LayerNumber}", number);
|
2025-05-31 19:26:02 +02:00
|
|
|
return Unauthorized();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var config = _db.Layers
|
|
|
|
|
.Include(x => x.Records)
|
|
|
|
|
.AsNoTracking()
|
|
|
|
|
.First(x => x.Number == number && !x.IsDeleted);
|
|
|
|
|
|
|
|
|
|
if (config is null)
|
|
|
|
|
{
|
2025-06-02 18:53:25 +02:00
|
|
|
_logger.LogWarning("Configuration: Layer {LayerNumber} not found", number);
|
2025-05-31 19:26:02 +02:00
|
|
|
return BadRequest();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var type = config.Records?.Where(x => x.Code == "Type").FirstOrDefault();
|
|
|
|
|
if (type is null || type.Desc1 != "ExternalConfiguration")
|
|
|
|
|
{
|
2025-06-02 18:53:25 +02:00
|
|
|
_logger.LogWarning("Configuration: Layer {LayerNumber} is not ExternalConfiguration type", number);
|
2025-05-31 19:26:02 +02:00
|
|
|
return BadRequest();
|
|
|
|
|
}
|
|
|
|
|
|
2025-06-02 18:53:25 +02:00
|
|
|
_logger.LogInformation("Configuration: Sending configuration for layer {LayerNumber}", number);
|
2025-05-31 19:26:02 +02:00
|
|
|
return Ok(config);
|
|
|
|
|
}
|
2025-06-02 18:53:25 +02:00
|
|
|
catch (Exception e)
|
2025-05-31 19:26:02 +02:00
|
|
|
{
|
2025-06-02 18:53:25 +02:00
|
|
|
_logger.LogError(e, "Configuration: Error occurred while processing layer {LayerNumber}", number);
|
2025-05-31 19:26:02 +02:00
|
|
|
return BadRequest();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
[HttpGet]
|
|
|
|
|
[Route("exportToGoogleSheet/{id:guid}")]
|
|
|
|
|
public IActionResult ExportToGoogleSheet(Guid id)
|
|
|
|
|
{
|
|
|
|
|
if (_googleSheetValues is null)
|
|
|
|
|
{
|
2025-06-02 18:53:25 +02:00
|
|
|
_logger.LogError("Export: Google Sheets API not initialized");
|
2025-05-31 19:26:02 +02:00
|
|
|
throw new Exception("Google Sheets API not initialized");
|
|
|
|
|
}
|
|
|
|
|
|
2025-06-02 18:53:25 +02:00
|
|
|
try
|
|
|
|
|
{
|
|
|
|
|
var layer = _db.Layers
|
|
|
|
|
.Include(x => x.Records!.OrderByDescending(y => y.Code)).AsNoTracking().First(x => x.Id == id && !x.IsDeleted);
|
2025-05-31 19:26:02 +02:00
|
|
|
|
2025-06-02 18:53:25 +02:00
|
|
|
var export = _pluginManager.GetExporter("GoogleSheet");
|
|
|
|
|
if (export == null)
|
|
|
|
|
{
|
|
|
|
|
_logger.LogError("Export: GoogleSheet exporter not found for layer {LayerId}", id);
|
|
|
|
|
throw new Exception("GoogleSheet exporter not found");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
_logger.LogInformation("Export: Starting GoogleSheet export for layer {LayerId} {LayerName}", id, layer.Name);
|
|
|
|
|
export.Export(layer);
|
|
|
|
|
_logger.LogInformation("Export: Successfully exported layer {LayerId} to GoogleSheet", id);
|
2025-06-06 20:23:36 +02:00
|
|
|
|
2025-06-02 18:53:25 +02:00
|
|
|
return Ok(true);
|
|
|
|
|
}
|
|
|
|
|
catch (Exception e)
|
2025-06-02 16:54:33 +02:00
|
|
|
{
|
2025-06-02 18:53:25 +02:00
|
|
|
_logger.LogError(e, "Export: Failed to export layer {LayerId} to GoogleSheet", id);
|
|
|
|
|
throw;
|
2025-06-02 16:54:33 +02:00
|
|
|
}
|
2025-05-31 19:26:02 +02:00
|
|
|
}
|
|
|
|
|
[HttpGet]
|
|
|
|
|
[Route("ProcessQueue/{apiKey}")]
|
|
|
|
|
[AllowAnonymous]
|
|
|
|
|
public IActionResult ProcessQueue(string apiKey)
|
|
|
|
|
{
|
2025-06-02 18:53:25 +02:00
|
|
|
if (Request.Host.Value != _configuration["apiLocalUrl"] || apiKey != _configuration["apiKey"])
|
2025-05-31 19:26:02 +02:00
|
|
|
{
|
2025-06-02 18:53:25 +02:00
|
|
|
_logger.LogWarning("ProcessQueue: Unauthorized request with apiKey {ApiKey}", apiKey);
|
|
|
|
|
return Unauthorized();
|
2025-05-31 19:26:02 +02:00
|
|
|
}
|
2025-06-02 18:53:25 +02:00
|
|
|
|
|
|
|
|
_logger.LogInformation("ProcessQueue: Starting queue processing");
|
|
|
|
|
// Queue processing implementation would go here
|
2025-05-31 19:26:02 +02:00
|
|
|
return Ok();
|
|
|
|
|
}
|
|
|
|
|
[HttpGet]
|
|
|
|
|
[Route("AutoImport/{apiKey}/{nameFilter}")]
|
|
|
|
|
[AllowAnonymous]
|
|
|
|
|
public IActionResult AutoImport(string apiKey, string nameFilter)
|
|
|
|
|
{
|
2025-11-19 19:03:04 +01:00
|
|
|
if (apiKey != _configuration["apiKey"])
|
2025-05-31 19:26:02 +02:00
|
|
|
{
|
2025-06-02 18:53:25 +02:00
|
|
|
_logger.LogWarning("AutoImport: Unauthorized request with apiKey {ApiKey}", apiKey);
|
2025-05-31 19:26:02 +02:00
|
|
|
return Unauthorized();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (_googleSheetValues is null)
|
|
|
|
|
{
|
2025-06-02 18:53:25 +02:00
|
|
|
_logger.LogError("AutoImport: Google Sheets API not initialized");
|
2025-05-31 19:26:02 +02:00
|
|
|
throw new Exception("Google Sheets API not initialized");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var importWorkerLayers = _db.Layers
|
|
|
|
|
.Include(x => x.Records)
|
|
|
|
|
.Where(x =>
|
|
|
|
|
x.Name != null && x.Name.Contains(nameFilter) &&
|
|
|
|
|
x.Records!.Any(y => y.Code == "Type" && y.Desc1 == "ImportWorker") &&
|
2025-06-02 20:11:29 +02:00
|
|
|
x.Records!.Any(y => y.Code == "IsEnabled" && y.Desc1 == "True"
|
|
|
|
|
)
|
2025-05-31 19:26:02 +02:00
|
|
|
)
|
|
|
|
|
.OrderByDescending(x => x.CreatedAt)
|
|
|
|
|
.AsNoTracking()
|
|
|
|
|
.ToList();
|
2025-06-02 18:53:25 +02:00
|
|
|
|
2025-06-06 20:23:36 +02:00
|
|
|
_logger.LogInformation("AutoImport: Starting import with filter {NameFilter}, found {LayerCount} layers",
|
2025-06-02 18:53:25 +02:00
|
|
|
nameFilter, importWorkerLayers.Count);
|
|
|
|
|
|
2025-05-31 19:26:02 +02:00
|
|
|
try
|
|
|
|
|
{
|
|
|
|
|
if (importWorkerLayers.Count == 0)
|
|
|
|
|
{
|
2025-06-02 18:53:25 +02:00
|
|
|
_logger.LogInformation("AutoImport: No layers to import");
|
2025-05-31 19:26:02 +02:00
|
|
|
return Ok();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
foreach (var importWorker in importWorkerLayers)
|
|
|
|
|
{
|
|
|
|
|
try
|
|
|
|
|
{
|
2025-06-02 18:53:25 +02:00
|
|
|
var type = importWorker.Records!.FirstOrDefault(x => x.Code == "ImportType")?.Desc1 ?? "Standard";
|
|
|
|
|
var source = importWorker.Records!.FirstOrDefault(x => x.Code == "Source")?.Desc1 ?? "GoogleSheet";
|
2025-06-06 20:23:36 +02:00
|
|
|
|
2025-06-08 11:48:31 +02:00
|
|
|
var plugin = importWorker.Records!.FirstOrDefault(x => x.Code == "Plugin")?.Desc1;
|
|
|
|
|
if (plugin == null)
|
|
|
|
|
{
|
|
|
|
|
_logger.LogError("AutoImport: Plugin not found for layer {LayerName} ({LayerId}), skipping",
|
|
|
|
|
importWorker.Name, importWorker.Id);
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
2025-06-06 20:23:36 +02:00
|
|
|
_logger.LogInformation("AutoImport: Processing layer {LayerName} with type {ImportType} and source {Source}",
|
2025-06-02 18:53:25 +02:00
|
|
|
importWorker.Name, type, source);
|
|
|
|
|
|
2025-05-31 19:26:02 +02:00
|
|
|
if (source == "DataInbox" && type == "Import-D3")
|
|
|
|
|
{
|
2025-06-08 11:48:31 +02:00
|
|
|
var d3Importer = _pluginManager.GetImporter(plugin);
|
2025-06-02 16:54:33 +02:00
|
|
|
if (d3Importer == null)
|
|
|
|
|
{
|
|
|
|
|
throw new Exception("MorskaD3 importer not found");
|
|
|
|
|
}
|
2025-05-31 19:26:02 +02:00
|
|
|
d3Importer.Import(importWorker);
|
2025-06-06 20:23:36 +02:00
|
|
|
|
|
|
|
|
_logger.LogInformation("AutoImport: Successfully processed D3 import for {LayerName} ({LayerId})",
|
2025-06-02 18:53:25 +02:00
|
|
|
importWorker.Name, importWorker.Id);
|
2025-05-31 19:26:02 +02:00
|
|
|
continue;
|
|
|
|
|
}
|
2025-06-02 18:53:25 +02:00
|
|
|
|
2025-05-31 19:26:02 +02:00
|
|
|
switch (type)
|
|
|
|
|
{
|
|
|
|
|
case "D1":
|
2025-06-10 18:37:11 +02:00
|
|
|
var d1Importer = _pluginManager.GetImporter(plugin);
|
|
|
|
|
if (d1Importer == null)
|
2025-06-02 16:54:33 +02:00
|
|
|
{
|
|
|
|
|
throw new Exception("MorskaD1 importer not found");
|
|
|
|
|
}
|
2025-06-10 18:37:11 +02:00
|
|
|
d1Importer.Import(importWorker);
|
2025-05-31 19:26:02 +02:00
|
|
|
Thread.Sleep(5000); // be aware of GSheet API quota
|
|
|
|
|
|
2025-06-06 20:23:36 +02:00
|
|
|
_logger.LogInformation("AutoImport: Successfully processed D1 import for {LayerName} ({LayerId})",
|
2025-06-02 18:53:25 +02:00
|
|
|
importWorker.Name, importWorker.Id);
|
2025-05-31 19:26:02 +02:00
|
|
|
break;
|
2025-06-02 18:53:25 +02:00
|
|
|
|
2025-05-31 19:26:02 +02:00
|
|
|
case "FK2":
|
2025-06-10 18:37:11 +02:00
|
|
|
var fk2Importer = _pluginManager.GetImporter(plugin);
|
|
|
|
|
if (fk2Importer == null)
|
2025-05-31 19:26:02 +02:00
|
|
|
{
|
2025-06-02 18:53:25 +02:00
|
|
|
throw new Exception("MorskaFK2 importer not found");
|
2025-05-31 19:26:02 +02:00
|
|
|
}
|
2025-06-10 18:37:11 +02:00
|
|
|
fk2Importer.Import(importWorker);
|
2025-06-02 18:53:25 +02:00
|
|
|
Thread.Sleep(5000); // be aware of GSheet API quota
|
|
|
|
|
|
2025-06-06 20:23:36 +02:00
|
|
|
_logger.LogInformation("AutoImport: Successfully processed FK2 import for {LayerName} ({LayerId})",
|
2025-06-02 18:53:25 +02:00
|
|
|
importWorker.Name, importWorker.Id);
|
|
|
|
|
break;
|
|
|
|
|
|
2025-05-31 19:26:02 +02:00
|
|
|
default:
|
2025-06-02 18:53:25 +02:00
|
|
|
var startDate = importWorker.Records!.FirstOrDefault(x => x.Code == "StartDate")?.Desc1;
|
|
|
|
|
if (startDate == null)
|
2025-05-31 19:26:02 +02:00
|
|
|
{
|
2025-06-02 18:53:25 +02:00
|
|
|
throw new Exception("StartDate record not found");
|
|
|
|
|
}
|
2025-05-31 19:26:02 +02:00
|
|
|
|
2025-06-02 18:53:25 +02:00
|
|
|
var endDate = importWorker.Records!.First(x => x.Code == "EndDate").Desc1;
|
|
|
|
|
if (endDate == null)
|
|
|
|
|
{
|
|
|
|
|
throw new Exception("EndDate record not found");
|
|
|
|
|
}
|
2025-05-31 19:26:02 +02:00
|
|
|
|
2025-06-02 18:53:25 +02:00
|
|
|
var startDateParsed = DateTime.ParseExact(startDate, "yyyy.MM.dd", null);
|
|
|
|
|
var endDateParsed = DateTime.ParseExact(endDate, "yyyy.MM.dd", null);
|
2025-06-06 20:23:36 +02:00
|
|
|
|
2025-06-02 18:53:25 +02:00
|
|
|
if (startDateParsed.Date <= DateTime.UtcNow.Date && endDateParsed.Date >= DateTime.UtcNow.Date)
|
|
|
|
|
{
|
2025-06-08 11:48:31 +02:00
|
|
|
var importer = _pluginManager.GetImporter(plugin);
|
2025-06-02 18:53:25 +02:00
|
|
|
if (importer == null)
|
2025-05-31 19:26:02 +02:00
|
|
|
{
|
2025-06-02 18:53:25 +02:00
|
|
|
throw new Exception("MorskaImporter not found");
|
2025-05-31 19:26:02 +02:00
|
|
|
}
|
2025-06-02 18:53:25 +02:00
|
|
|
importer.Import(importWorker);
|
|
|
|
|
Thread.Sleep(5000); // be aware of GSheet API quota
|
|
|
|
|
|
2025-06-06 20:23:36 +02:00
|
|
|
_logger.LogInformation("AutoImport: Successfully processed standard import for {LayerName} ({LayerId})",
|
2025-06-02 18:53:25 +02:00
|
|
|
importWorker.Name, importWorker.Id);
|
|
|
|
|
}
|
|
|
|
|
else if (IsImportedLayerUpToDate(importWorker) == false)
|
|
|
|
|
{
|
2025-06-08 11:48:31 +02:00
|
|
|
var importer = _pluginManager.GetImporter(plugin);
|
2025-06-02 18:53:25 +02:00
|
|
|
if (importer == null)
|
2025-05-31 19:26:02 +02:00
|
|
|
{
|
2025-06-02 18:53:25 +02:00
|
|
|
throw new Exception("MorskaImporter not found");
|
2025-05-31 19:26:02 +02:00
|
|
|
}
|
2025-06-02 18:53:25 +02:00
|
|
|
importer.Import(importWorker);
|
|
|
|
|
Thread.Sleep(5000); // be aware of GSheet API quota
|
2025-05-31 19:26:02 +02:00
|
|
|
|
2025-06-06 20:23:36 +02:00
|
|
|
_logger.LogWarning("AutoImport: Reimported out-of-date layer {LayerName} ({LayerId})",
|
2025-06-02 18:53:25 +02:00
|
|
|
importWorker.Name, importWorker.Id);
|
2025-05-31 19:26:02 +02:00
|
|
|
}
|
2025-06-02 18:53:25 +02:00
|
|
|
else
|
|
|
|
|
{
|
2025-06-06 20:23:36 +02:00
|
|
|
_logger.LogInformation("AutoImport: Layer {LayerName} ({LayerId}) is up to date, skipping",
|
2025-06-02 18:53:25 +02:00
|
|
|
importWorker.Name, importWorker.Id);
|
|
|
|
|
}
|
|
|
|
|
break;
|
2025-05-31 19:26:02 +02:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
catch (Exception e)
|
|
|
|
|
{
|
2025-06-06 20:23:36 +02:00
|
|
|
_logger.LogError(e, "AutoImport: Failed to process layer {LayerName} ({LayerId})",
|
2025-06-02 18:53:25 +02:00
|
|
|
importWorker.Name, importWorker.Id);
|
2025-05-31 19:26:02 +02:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-06-02 18:53:25 +02:00
|
|
|
_logger.LogInformation("AutoImport: Completed processing {LayerCount} layers", importWorkerLayers.Count);
|
2025-05-31 19:26:02 +02:00
|
|
|
return Ok();
|
|
|
|
|
}
|
|
|
|
|
catch (Exception e)
|
|
|
|
|
{
|
2025-06-02 18:53:25 +02:00
|
|
|
_logger.LogError(e, "AutoImport: Process error");
|
2025-12-05 21:37:15 +01:00
|
|
|
return BadRequest("An error occurred processing your request");
|
2025-05-31 19:26:02 +02:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
[HttpGet]
|
|
|
|
|
[Route("AutoProcess/{apiKey}")]
|
|
|
|
|
[AllowAnonymous]
|
|
|
|
|
public IActionResult AutoProcess(string apiKey)
|
|
|
|
|
{
|
2025-11-25 14:09:53 +01:00
|
|
|
if (apiKey != _configuration["apiKey"])
|
2025-05-31 19:26:02 +02:00
|
|
|
{
|
2025-11-25 14:09:53 +01:00
|
|
|
_logger.LogWarning("AutoImport: Unauthorized request with apiKey {ApiKey}", apiKey);
|
2025-05-31 19:26:02 +02:00
|
|
|
return Unauthorized();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (_googleSheetValues is null)
|
|
|
|
|
{
|
2025-06-02 18:53:25 +02:00
|
|
|
_logger.LogError("AutoProcess: Google Sheets API not initialized");
|
2025-05-31 19:26:02 +02:00
|
|
|
throw new Exception("Google Sheets API not initialized");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
string[] processTypes =
|
|
|
|
|
[
|
|
|
|
|
"T3-SingleSource",
|
|
|
|
|
"T3-SourceYearSummary",
|
2025-06-02 18:53:25 +02:00
|
|
|
"T3-MultiSourceSummary",
|
|
|
|
|
"T3-MultiSourceYearSummary",
|
2025-05-31 19:26:02 +02:00
|
|
|
"T4-SingleSource",
|
|
|
|
|
"T5-LastValues",
|
|
|
|
|
"T1-R1",
|
|
|
|
|
"T4-R2",
|
2025-06-16 20:36:48 +02:00
|
|
|
"T1-R3",
|
|
|
|
|
"D6"
|
2025-05-31 19:26:02 +02:00
|
|
|
];
|
|
|
|
|
|
2025-06-02 18:53:25 +02:00
|
|
|
_logger.LogInformation("AutoProcess: Starting processing for {ProcessTypeCount} process types", processTypes.Length);
|
|
|
|
|
|
2025-05-31 19:26:02 +02:00
|
|
|
foreach (var type in processTypes)
|
|
|
|
|
{
|
|
|
|
|
try
|
|
|
|
|
{
|
|
|
|
|
var processWorkerLayers = _db.Layers
|
|
|
|
|
.Include(x => x.Records)
|
|
|
|
|
.Where(x =>
|
|
|
|
|
x.Records!.Any(y => y.Code == "Type" && y.Desc1 == "ProcessWorker") &&
|
|
|
|
|
x.Records!.Any(y => y.Code == "IsEnabled" && y.Desc1 == "True") &&
|
|
|
|
|
x.Records!.Any(y => y.Code == "ProcessType" && y.Desc1 == type)
|
|
|
|
|
)
|
|
|
|
|
.OrderBy(x => x.CreatedAt)
|
|
|
|
|
.AsNoTracking()
|
|
|
|
|
.ToList();
|
|
|
|
|
|
2025-06-06 20:23:36 +02:00
|
|
|
_logger.LogInformation("AutoProcess: Processing type {ProcessType}, found {LayerCount} layers",
|
2025-06-02 18:53:25 +02:00
|
|
|
type, processWorkerLayers.Count);
|
|
|
|
|
|
2025-05-31 19:26:02 +02:00
|
|
|
foreach (var processWorker in processWorkerLayers)
|
|
|
|
|
{
|
|
|
|
|
try
|
|
|
|
|
{
|
|
|
|
|
ProcessLayer(processWorker);
|
2025-06-06 20:23:36 +02:00
|
|
|
_logger.LogInformation("AutoProcess: Successfully processed {LayerName} ({LayerId}) with type {ProcessType}",
|
2025-06-02 18:53:25 +02:00
|
|
|
processWorker.Name, processWorker.Id, type);
|
2025-05-31 19:26:02 +02:00
|
|
|
}
|
|
|
|
|
catch (Exception e)
|
|
|
|
|
{
|
2025-06-06 20:23:36 +02:00
|
|
|
_logger.LogError(e, "AutoProcess: Failed to process {LayerName} ({LayerId}) with type {ProcessType}",
|
2025-06-02 18:53:25 +02:00
|
|
|
processWorker.Name, processWorker.Id, type);
|
2025-05-31 19:26:02 +02:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
catch (Exception e)
|
|
|
|
|
{
|
2025-06-02 18:53:25 +02:00
|
|
|
_logger.LogError(e, "AutoProcess: Error processing type {ProcessType}", type);
|
2025-05-31 19:26:02 +02:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-06-02 18:53:25 +02:00
|
|
|
_logger.LogInformation("AutoProcess: Completed processing all process types");
|
2025-05-31 19:26:02 +02:00
|
|
|
return Ok();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void ProcessLayer(Layer processWorker)
|
|
|
|
|
{
|
|
|
|
|
if (_googleSheetValues == null)
|
|
|
|
|
{
|
|
|
|
|
throw new Exception("Google Sheets API not initialized");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var year = processWorker.Records?.SingleOrDefault(x => x.Code == "Year")?.Desc1;
|
|
|
|
|
if (year == null)
|
|
|
|
|
{
|
2025-06-02 18:53:25 +02:00
|
|
|
throw new Exception("Year record not found");
|
2025-05-31 19:26:02 +02:00
|
|
|
}
|
|
|
|
|
|
2025-06-08 11:48:31 +02:00
|
|
|
var plugin = processWorker.Records?.SingleOrDefault(x => x.Code == "Plugin")?.Desc1;
|
|
|
|
|
if (plugin == null)
|
|
|
|
|
{
|
|
|
|
|
throw new Exception("Plugin record not found");
|
|
|
|
|
}
|
|
|
|
|
|
2025-05-31 19:26:02 +02:00
|
|
|
var processType = processWorker.Records?.SingleOrDefault(x => x.Code == "ProcessType")?.Desc1;
|
|
|
|
|
switch (processType)
|
|
|
|
|
{
|
|
|
|
|
case null:
|
|
|
|
|
throw new Exception("ProcessType record not found");
|
|
|
|
|
case "T3-SourceYearSummary":
|
|
|
|
|
{
|
2025-06-08 11:48:31 +02:00
|
|
|
var processor = _pluginManager.GetProcessor(plugin);
|
2025-06-02 16:54:33 +02:00
|
|
|
if (processor == null)
|
|
|
|
|
{
|
|
|
|
|
throw new Exception("T3.SourceYearSummary processor not found");
|
|
|
|
|
}
|
2025-05-31 19:26:02 +02:00
|
|
|
processor.Process(processWorker);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
case "T3-MultiSourceYearSummary":
|
|
|
|
|
{
|
2025-06-08 11:48:31 +02:00
|
|
|
var processor = _pluginManager.GetProcessor(plugin);
|
2025-06-02 16:54:33 +02:00
|
|
|
if (processor == null)
|
|
|
|
|
{
|
|
|
|
|
throw new Exception("T3.MultiSourceYearSummary processor not found");
|
|
|
|
|
}
|
2025-05-31 19:26:02 +02:00
|
|
|
processor.Process(processWorker);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
case "T3-MultiSourceCopySelectedCodesYearSummary":
|
|
|
|
|
{
|
2025-06-08 11:48:31 +02:00
|
|
|
var processor = _pluginManager.GetProcessor(plugin);
|
2025-06-02 16:54:33 +02:00
|
|
|
if (processor == null)
|
|
|
|
|
{
|
|
|
|
|
throw new Exception("T3.MultiSourceCopySelectedCodesYearSummary processor not found");
|
|
|
|
|
}
|
2025-05-31 19:26:02 +02:00
|
|
|
processor.Process(processWorker);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
case "T1-R1":
|
|
|
|
|
{
|
2025-06-08 11:48:31 +02:00
|
|
|
var processor = _pluginManager.GetProcessor(plugin);
|
2025-06-02 16:54:33 +02:00
|
|
|
if (processor == null)
|
|
|
|
|
{
|
|
|
|
|
throw new Exception("T1.R1 processor not found");
|
|
|
|
|
}
|
2025-05-31 19:26:02 +02:00
|
|
|
processor.Process(processWorker);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
case "T4-R2":
|
|
|
|
|
{
|
2025-06-08 11:48:31 +02:00
|
|
|
var processor = _pluginManager.GetProcessor(plugin);
|
2025-06-02 16:54:33 +02:00
|
|
|
if (processor == null)
|
|
|
|
|
{
|
|
|
|
|
throw new Exception("T4.R2 processor not found");
|
|
|
|
|
}
|
2025-05-31 19:26:02 +02:00
|
|
|
processor.Process(processWorker);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
case "T1-R3":
|
|
|
|
|
{
|
2025-06-08 11:48:31 +02:00
|
|
|
var processor = _pluginManager.GetProcessor(plugin);
|
2025-06-02 16:54:33 +02:00
|
|
|
if (processor == null)
|
|
|
|
|
{
|
|
|
|
|
throw new Exception("T1.R3 processor not found");
|
|
|
|
|
}
|
2025-05-31 19:26:02 +02:00
|
|
|
processor.Process(processWorker);
|
|
|
|
|
return;
|
|
|
|
|
}
|
2025-06-16 20:36:48 +02:00
|
|
|
case "D6":
|
|
|
|
|
{
|
|
|
|
|
var processor = _pluginManager.GetProcessor(plugin);
|
|
|
|
|
if (processor == null)
|
|
|
|
|
{
|
|
|
|
|
throw new Exception("D6 processor not found");
|
|
|
|
|
}
|
|
|
|
|
processor.Process(processWorker);
|
|
|
|
|
return;
|
|
|
|
|
}
|
2025-05-31 19:26:02 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var month = processWorker.Records?.SingleOrDefault(x => x.Code == "Month")?.Desc1;
|
|
|
|
|
if (month == null)
|
|
|
|
|
{
|
|
|
|
|
throw new Exception("Month record not found");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
switch (processType!)
|
|
|
|
|
{
|
|
|
|
|
case "T3-SingleSource":
|
|
|
|
|
{
|
2025-06-08 11:48:31 +02:00
|
|
|
var t3SingleSource = _pluginManager.GetProcessor(plugin);
|
2025-06-02 16:54:33 +02:00
|
|
|
if (t3SingleSource == null)
|
|
|
|
|
{
|
|
|
|
|
throw new Exception("T3.SingleSource processor not found");
|
|
|
|
|
}
|
2025-05-31 19:26:02 +02:00
|
|
|
t3SingleSource.Process(processWorker);
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
case "T4-SingleSource":
|
|
|
|
|
{
|
2025-06-08 11:48:31 +02:00
|
|
|
var t4SingleSource = _pluginManager.GetProcessor(plugin);
|
2025-06-02 16:54:33 +02:00
|
|
|
if (t4SingleSource == null)
|
|
|
|
|
{
|
|
|
|
|
throw new Exception("T4.SingleSource processor not found");
|
|
|
|
|
}
|
2025-05-31 19:26:02 +02:00
|
|
|
t4SingleSource.Process(processWorker);
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
case "T5-LastValues":
|
|
|
|
|
{
|
2025-06-08 11:48:31 +02:00
|
|
|
var t5LastValues = _pluginManager.GetProcessor(plugin);
|
2025-06-02 16:54:33 +02:00
|
|
|
if (t5LastValues == null)
|
|
|
|
|
{
|
|
|
|
|
throw new Exception("T5.LastValues processor not found");
|
|
|
|
|
}
|
2025-05-31 19:26:02 +02:00
|
|
|
t5LastValues.Process(processWorker);
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
case "T3-MultiSourceSummary":
|
|
|
|
|
{
|
2025-06-08 11:48:31 +02:00
|
|
|
var t3MultiSourceSummary = _pluginManager.GetProcessor(plugin);
|
2025-06-02 16:54:33 +02:00
|
|
|
if (t3MultiSourceSummary == null)
|
|
|
|
|
{
|
|
|
|
|
throw new Exception("T3.MultiSourceSummary processor not found");
|
|
|
|
|
}
|
2025-05-31 19:26:02 +02:00
|
|
|
t3MultiSourceSummary.Process(processWorker);
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
case "T3-MultiSourceCopySelectedCodes":
|
|
|
|
|
{
|
2025-06-08 11:48:31 +02:00
|
|
|
var t3MultiSourceCopySelectedCode = _pluginManager.GetProcessor(plugin);
|
2025-06-02 16:54:33 +02:00
|
|
|
if (t3MultiSourceCopySelectedCode == null)
|
|
|
|
|
{
|
|
|
|
|
throw new Exception("T3.MultiSourceCopySelectedCodes processor not found");
|
|
|
|
|
}
|
2025-05-31 19:26:02 +02:00
|
|
|
t3MultiSourceCopySelectedCode.Process(processWorker);
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-06-06 20:23:36 +02:00
|
|
|
[HttpGet]
|
2025-06-07 16:34:36 +02:00
|
|
|
[Route("CheckProcessors")]
|
2025-06-06 20:23:36 +02:00
|
|
|
[AllowAnonymous]
|
2025-06-08 10:18:52 +02:00
|
|
|
|
2025-05-31 19:26:02 +02:00
|
|
|
private static void WriteToConsole(params string[] messages)
|
|
|
|
|
{
|
|
|
|
|
foreach (var message in messages)
|
|
|
|
|
{
|
|
|
|
|
Console.WriteLine($"DiunaLog: {message}");
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private bool IsImportedLayerUpToDate(Layer importWorker)
|
|
|
|
|
{
|
|
|
|
|
if (_googleSheetValues is null)
|
|
|
|
|
{
|
|
|
|
|
throw new Exception("Google Sheets API not initialized");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var newestLayer = _db.Layers
|
|
|
|
|
.Include(x => x.Records)
|
|
|
|
|
.Where(x => x.ParentId == importWorker.Id)
|
|
|
|
|
.OrderByDescending(x => x.CreatedAt)
|
|
|
|
|
.AsNoTracking()
|
|
|
|
|
.FirstOrDefault();
|
|
|
|
|
|
|
|
|
|
if (newestLayer is null)
|
|
|
|
|
{
|
2025-06-02 18:53:25 +02:00
|
|
|
_logger.LogDebug("IsImportedLayerUpToDate: No child layers found for {LayerName}, treating as up to date", importWorker.Name);
|
2025-05-31 19:26:02 +02:00
|
|
|
return true; // importWorker is not active yet, no check needed
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var sheetId = importWorker.Records!.FirstOrDefault(x => x.Code == "SheetId")?.Desc1;
|
|
|
|
|
if (sheetId == null)
|
|
|
|
|
{
|
|
|
|
|
throw new Exception($"SheetId not found, {importWorker.Name}");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var sheetTabName = importWorker.Records!.FirstOrDefault(x => x.Code == "SheetTabName")?.Desc1;
|
|
|
|
|
if (sheetTabName == null)
|
|
|
|
|
{
|
|
|
|
|
throw new Exception($"SheetTabName not found, {importWorker.Name}");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var dataRange = importWorker.Records!.FirstOrDefault(x => x.Code == "DataRange")?.Desc1;
|
|
|
|
|
if (dataRange == null)
|
|
|
|
|
{
|
|
|
|
|
throw new Exception($"DataRange not found, {importWorker.Name}");
|
|
|
|
|
}
|
|
|
|
|
|
2025-06-02 18:53:25 +02:00
|
|
|
try
|
|
|
|
|
{
|
|
|
|
|
var dataRangeResponse = _googleSheetValues.Get(sheetId, $"{sheetTabName}!{dataRange}").Execute();
|
|
|
|
|
var data = dataRangeResponse.Values;
|
2025-05-31 19:26:02 +02:00
|
|
|
|
2025-06-02 18:53:25 +02:00
|
|
|
var isUpToDate = true;
|
2025-05-31 19:26:02 +02:00
|
|
|
|
2025-06-02 18:53:25 +02:00
|
|
|
for (var i = 0; i < data[1].Count; i++)
|
2025-05-31 19:26:02 +02:00
|
|
|
{
|
2025-06-02 18:53:25 +02:00
|
|
|
if (data[0][i].ToString() == "") continue;
|
|
|
|
|
var record = newestLayer.Records!.FirstOrDefault(x => x.Code == data[0][i].ToString());
|
|
|
|
|
if (record == null)
|
|
|
|
|
{
|
2025-06-06 20:23:36 +02:00
|
|
|
_logger.LogDebug("IsImportedLayerUpToDate: Code {Code} not found in DiunaBI for layer {LayerName}",
|
2025-06-02 18:53:25 +02:00
|
|
|
data[0][i].ToString(), importWorker.Name);
|
|
|
|
|
isUpToDate = false;
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!double.TryParse(data[1][i].ToString(), CultureInfo.GetCultureInfo("pl-PL"), out var value) ||
|
|
|
|
|
double.Abs((double)(record.Value1 - value)!) < 0.01) continue;
|
2025-05-31 19:26:02 +02:00
|
|
|
isUpToDate = false;
|
|
|
|
|
}
|
|
|
|
|
|
2025-06-02 18:53:25 +02:00
|
|
|
foreach (var record in newestLayer.Records!)
|
2025-05-31 19:26:02 +02:00
|
|
|
{
|
2025-06-02 18:53:25 +02:00
|
|
|
if (data[0].Contains(record.Code))
|
|
|
|
|
{
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
2025-06-06 20:23:36 +02:00
|
|
|
_logger.LogDebug("IsImportedLayerUpToDate: Code {Code} not found in GoogleSheet for layer {LayerName}",
|
2025-06-02 18:53:25 +02:00
|
|
|
record.Code, importWorker.Name);
|
|
|
|
|
isUpToDate = false;
|
2025-05-31 19:26:02 +02:00
|
|
|
}
|
|
|
|
|
|
2025-06-06 20:23:36 +02:00
|
|
|
_logger.LogDebug("IsImportedLayerUpToDate: Layer {LayerName} is {Status}",
|
2025-06-02 18:53:25 +02:00
|
|
|
importWorker.Name, isUpToDate ? "up to date" : "outdated");
|
2025-05-31 19:26:02 +02:00
|
|
|
|
2025-06-02 18:53:25 +02:00
|
|
|
return isUpToDate;
|
|
|
|
|
}
|
|
|
|
|
catch (Exception e)
|
|
|
|
|
{
|
|
|
|
|
_logger.LogError(e, "IsImportedLayerUpToDate: Error checking if layer {LayerName} is up to date", importWorker.Name);
|
|
|
|
|
throw;
|
|
|
|
|
}
|
2025-05-31 19:26:02 +02:00
|
|
|
}
|
2025-12-01 17:56:17 +01:00
|
|
|
|
|
|
|
|
// 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);
|
|
|
|
|
|
2025-12-01 18:37:09 +01:00
|
|
|
// Capture history
|
|
|
|
|
CaptureRecordHistory(record, RecordChangeType.Created, Guid.Parse(userId));
|
|
|
|
|
|
2025-12-01 17:56:17 +01:00
|
|
|
// 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);
|
2025-12-05 21:37:15 +01:00
|
|
|
return BadRequest("An error occurred processing your request");
|
2025-12-01 17:56:17 +01:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
[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");
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-01 18:37:09 +01:00
|
|
|
// Capture old values before updating
|
|
|
|
|
var oldCode = record.Code;
|
|
|
|
|
var oldDesc1 = record.Desc1;
|
|
|
|
|
|
2025-12-01 17:56:17 +01:00
|
|
|
record.Desc1 = recordDto.Desc1;
|
|
|
|
|
record.ModifiedAt = DateTime.UtcNow;
|
|
|
|
|
record.ModifiedById = Guid.Parse(userId);
|
|
|
|
|
|
2025-12-01 18:37:09 +01:00
|
|
|
// Capture history
|
|
|
|
|
CaptureRecordHistory(record, RecordChangeType.Updated, Guid.Parse(userId), oldCode, oldDesc1);
|
|
|
|
|
|
2025-12-01 17:56:17 +01:00
|
|
|
// 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);
|
2025-12-05 21:37:15 +01:00
|
|
|
return BadRequest("An error occurred processing your request");
|
2025-12-01 17:56:17 +01:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
[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");
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-01 18:37:09 +01:00
|
|
|
// Capture history before deleting
|
|
|
|
|
CaptureRecordHistory(record, RecordChangeType.Deleted, Guid.Parse(userId));
|
|
|
|
|
|
2025-12-01 17:56:17 +01:00
|
|
|
_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);
|
2025-12-05 21:37:15 +01:00
|
|
|
return BadRequest("An error occurred processing your request");
|
2025-12-01 17:56:17 +01:00
|
|
|
}
|
|
|
|
|
}
|
2025-12-01 18:37:09 +01:00
|
|
|
|
|
|
|
|
[HttpGet]
|
|
|
|
|
[Route("{layerId:guid}/records/{recordId:guid}/history")]
|
|
|
|
|
public IActionResult GetRecordHistory(Guid layerId, Guid recordId)
|
|
|
|
|
{
|
|
|
|
|
try
|
|
|
|
|
{
|
|
|
|
|
var history = _db.RecordHistory
|
|
|
|
|
.Include(h => h.ChangedBy)
|
|
|
|
|
.Where(h => h.RecordId == recordId && h.LayerId == layerId)
|
|
|
|
|
.OrderByDescending(h => h.ChangedAt)
|
|
|
|
|
.AsNoTracking()
|
|
|
|
|
.Select(h => new RecordHistoryDto
|
|
|
|
|
{
|
|
|
|
|
Id = h.Id,
|
|
|
|
|
RecordId = h.RecordId,
|
|
|
|
|
LayerId = h.LayerId,
|
|
|
|
|
ChangedAt = h.ChangedAt,
|
|
|
|
|
ChangedById = h.ChangedById,
|
|
|
|
|
ChangedByName = h.ChangedBy != null ? h.ChangedBy.UserName ?? h.ChangedBy.Email : "Unknown",
|
|
|
|
|
ChangeType = h.ChangeType.ToString(),
|
|
|
|
|
Code = h.Code,
|
|
|
|
|
Desc1 = h.Desc1,
|
|
|
|
|
ChangedFields = h.ChangedFields,
|
|
|
|
|
ChangesSummary = h.ChangesSummary,
|
|
|
|
|
FormattedChange = FormatHistoryChange(h)
|
|
|
|
|
})
|
|
|
|
|
.ToList();
|
|
|
|
|
|
|
|
|
|
_logger.LogDebug("GetRecordHistory: Retrieved {Count} history entries for record {RecordId}", history.Count, recordId);
|
|
|
|
|
|
|
|
|
|
return Ok(history);
|
|
|
|
|
}
|
|
|
|
|
catch (Exception e)
|
|
|
|
|
{
|
|
|
|
|
_logger.LogError(e, "GetRecordHistory: Error retrieving history for record {RecordId}", recordId);
|
2025-12-05 21:37:15 +01:00
|
|
|
return BadRequest("An error occurred processing your request");
|
2025-12-01 18:37:09 +01:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
[HttpGet]
|
|
|
|
|
[Route("{layerId:guid}/records/deleted")]
|
|
|
|
|
public IActionResult GetDeletedRecords(Guid layerId)
|
|
|
|
|
{
|
|
|
|
|
try
|
|
|
|
|
{
|
|
|
|
|
// Get the most recent "Deleted" history entry for each unique RecordId in this layer
|
2025-12-02 13:14:09 +01:00
|
|
|
// First, get all deleted record history entries
|
|
|
|
|
var deletedHistoryEntries = _db.RecordHistory
|
2025-12-01 18:37:09 +01:00
|
|
|
.Where(h => h.LayerId == layerId && h.ChangeType == RecordChangeType.Deleted)
|
2025-12-02 13:14:09 +01:00
|
|
|
.ToList();
|
|
|
|
|
|
|
|
|
|
// Group in memory and get the most recent deletion for each record
|
|
|
|
|
var mostRecentDeletes = deletedHistoryEntries
|
2025-12-01 18:37:09 +01:00
|
|
|
.GroupBy(h => h.RecordId)
|
2025-12-02 13:14:09 +01:00
|
|
|
.Select(g => g.OrderByDescending(h => h.ChangedAt).First())
|
|
|
|
|
.ToList();
|
|
|
|
|
|
|
|
|
|
// Get all unique user IDs from the history entries
|
|
|
|
|
var userIds = mostRecentDeletes.Select(h => h.ChangedById).Distinct().ToList();
|
|
|
|
|
|
|
|
|
|
// Load the users
|
|
|
|
|
var users = _db.Users
|
|
|
|
|
.Where(u => userIds.Contains(u.Id))
|
|
|
|
|
.ToDictionary(u => u.Id, u => u.UserName ?? string.Empty);
|
|
|
|
|
|
|
|
|
|
// Build the DTOs
|
|
|
|
|
var deletedRecords = mostRecentDeletes
|
2025-12-01 18:37:09 +01:00
|
|
|
.Select(h => new DeletedRecordDto
|
|
|
|
|
{
|
2025-12-02 13:14:09 +01:00
|
|
|
RecordId = h.RecordId,
|
2025-12-01 18:37:09 +01:00
|
|
|
Code = h.Code,
|
|
|
|
|
Desc1 = h.Desc1,
|
|
|
|
|
DeletedAt = h.ChangedAt,
|
|
|
|
|
DeletedById = h.ChangedById,
|
2025-12-02 13:14:09 +01:00
|
|
|
DeletedByName = users.TryGetValue(h.ChangedById, out var userName) ? userName : string.Empty
|
2025-12-01 18:37:09 +01:00
|
|
|
})
|
|
|
|
|
.OrderByDescending(d => d.DeletedAt)
|
|
|
|
|
.ToList();
|
|
|
|
|
|
|
|
|
|
_logger.LogDebug("GetDeletedRecords: Retrieved {Count} deleted records for layer {LayerId}", deletedRecords.Count, layerId);
|
|
|
|
|
return Ok(deletedRecords);
|
|
|
|
|
}
|
|
|
|
|
catch (Exception e)
|
|
|
|
|
{
|
|
|
|
|
_logger.LogError(e, "GetDeletedRecords: Error retrieving deleted records for layer {LayerId}", layerId);
|
2025-12-05 21:37:15 +01:00
|
|
|
return BadRequest("An error occurred processing your request");
|
2025-12-01 18:37:09 +01:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Helper method to capture record history
|
|
|
|
|
private void CaptureRecordHistory(Record record, RecordChangeType changeType, Guid userId, string? oldCode = null, string? oldDesc1 = null)
|
|
|
|
|
{
|
|
|
|
|
var changedFields = new List<string>();
|
|
|
|
|
var changesSummary = new Dictionary<string, Dictionary<string, string?>>();
|
|
|
|
|
|
|
|
|
|
if (changeType == RecordChangeType.Updated)
|
|
|
|
|
{
|
|
|
|
|
if (oldCode != record.Code)
|
|
|
|
|
{
|
|
|
|
|
changedFields.Add("Code");
|
|
|
|
|
changesSummary["Code"] = new Dictionary<string, string?>
|
|
|
|
|
{
|
|
|
|
|
["old"] = oldCode,
|
|
|
|
|
["new"] = record.Code
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (oldDesc1 != record.Desc1)
|
|
|
|
|
{
|
|
|
|
|
changedFields.Add("Desc1");
|
|
|
|
|
changesSummary["Desc1"] = new Dictionary<string, string?>
|
|
|
|
|
{
|
|
|
|
|
["old"] = oldDesc1,
|
|
|
|
|
["new"] = record.Desc1
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var history = new RecordHistory
|
|
|
|
|
{
|
|
|
|
|
Id = Guid.NewGuid(),
|
|
|
|
|
RecordId = record.Id,
|
|
|
|
|
LayerId = record.LayerId,
|
|
|
|
|
ChangedAt = DateTime.UtcNow,
|
|
|
|
|
ChangedById = userId,
|
|
|
|
|
ChangeType = changeType,
|
|
|
|
|
Code = record.Code,
|
|
|
|
|
Desc1 = record.Desc1,
|
|
|
|
|
ChangedFields = changedFields.Any() ? string.Join(", ", changedFields) : null,
|
|
|
|
|
ChangesSummary = changesSummary.Any() ? JsonSerializer.Serialize(changesSummary) : null
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
_db.RecordHistory.Add(history);
|
|
|
|
|
_logger.LogInformation("CaptureRecordHistory: Captured {ChangeType} for record {RecordId}", changeType, record.Id);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Helper method to format history change for display
|
|
|
|
|
private static string FormatHistoryChange(RecordHistory h)
|
|
|
|
|
{
|
|
|
|
|
if (h.ChangeType == RecordChangeType.Created)
|
|
|
|
|
{
|
|
|
|
|
return $"Created record with Code: \"{h.Code}\", Description: \"{h.Desc1}\"";
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (h.ChangeType == RecordChangeType.Deleted)
|
|
|
|
|
{
|
|
|
|
|
return $"Deleted record Code: \"{h.Code}\", Description: \"{h.Desc1}\"";
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Updated
|
|
|
|
|
if (!string.IsNullOrEmpty(h.ChangesSummary))
|
|
|
|
|
{
|
|
|
|
|
try
|
|
|
|
|
{
|
|
|
|
|
var changes = JsonSerializer.Deserialize<Dictionary<string, Dictionary<string, string?>>>(h.ChangesSummary);
|
|
|
|
|
if (changes != null)
|
|
|
|
|
{
|
|
|
|
|
var parts = new List<string>();
|
|
|
|
|
foreach (var (field, values) in changes)
|
|
|
|
|
{
|
|
|
|
|
var oldVal = values.GetValueOrDefault("old") ?? "empty";
|
|
|
|
|
var newVal = values.GetValueOrDefault("new") ?? "empty";
|
|
|
|
|
parts.Add($"{field}: \"{oldVal}\" → \"{newVal}\"");
|
|
|
|
|
}
|
|
|
|
|
return $"Updated: {string.Join(", ", parts)}";
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
catch
|
|
|
|
|
{
|
|
|
|
|
// Fall back to simple message
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return $"Updated {h.ChangedFields ?? "record"}";
|
|
|
|
|
}
|
2025-05-31 19:26:02 +02:00
|
|
|
}
|