Compare commits
12 Commits
03dac34f5e
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 2b4678c26d | |||
| 8dedafdc07 | |||
| 6f9392f3d0 | |||
| dd31460cf0 | |||
| 33c9c03822 | |||
| 6636d9bed7 | |||
| 781fecbf42 | |||
| 10214805e3 | |||
| 2414fbe80c | |||
| 1fec0ae0f5 | |||
| b6c61238a5 | |||
| 087e439b54 |
@@ -23,6 +23,7 @@
|
||||
<ItemGroup>
|
||||
<Content Include="Scripts\gestiondenuncias_schema.sql" CopyToOutputDirectory="PreserveNewest" />
|
||||
<Content Include="Scripts\gestiondenuncias_envelope_encryption.sql" CopyToOutputDirectory="PreserveNewest" />
|
||||
<Content Include="Scripts\gestiondenuncias_gestiona_expediente_excepciones.sql" CopyToOutputDirectory="PreserveNewest" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
@@ -4,14 +4,21 @@ namespace ApiDenuncias.Configuration
|
||||
{
|
||||
public string ApiBase { get; set; } = null!;
|
||||
public string AccessToken { get; set; } = null!;
|
||||
public string UserLink { get; set; } = null!;
|
||||
public string GroupLink { get; set; } = null!;
|
||||
public string Location { get; set; } = null!;
|
||||
public string? ExternalProcedureId { get; set; }
|
||||
public string? CircuitTemplateId { get; set; }
|
||||
public string? CircuitSignerStampHref { get; set; }
|
||||
public string? ProcedureName { get; set; }
|
||||
public string? ExternalProcedureName { get; set; }
|
||||
public string? ExternalProcedureSiaCode { get; set; }
|
||||
public string? ManagementUnitGroupCode { get; set; }
|
||||
public string? CircuitTemplateName { get; set; }
|
||||
public string? CircuitNewComplaintTemplateName { get; set; }
|
||||
public string? CircuitUpdateTemplateName { get; set; }
|
||||
public string? CircuitUpdateSajTemplateName { get; set; }
|
||||
public string? CircuitUpdateSdiTemplateName { get; set; }
|
||||
public string? CircuitCommunicationSajTemplateName { get; set; }
|
||||
public string? CircuitCommunicationSdiTemplateName { get; set; }
|
||||
public string? CircuitSignerStampTitle { get; set; }
|
||||
public string? CircuitRecipientGroupHref { get; set; }
|
||||
public string? CircuitVersion { get; set; }
|
||||
public string? DocumentMetadataLanguage { get; set; }
|
||||
public string? DocumentMetadataType { get; set; }
|
||||
public string? DocumentMetadataSubtype { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -304,7 +304,15 @@ public sealed class AuthController : ControllerBase
|
||||
: session.Username.Trim();
|
||||
|
||||
_logger.LogInformation("Login GlobalLeaks validado para {Username}. Guardando sesion cifrada.", username);
|
||||
await _sessionStore.SaveAsync(username, password, session.Id, session.Role, cancellationToken);
|
||||
await _sessionStore.SaveAsync(
|
||||
username,
|
||||
password,
|
||||
session.Id,
|
||||
session.Role,
|
||||
session.DpopPrivateKey,
|
||||
session.ProofOfWorkToken,
|
||||
session.SessionExpiresAtUtc,
|
||||
cancellationToken);
|
||||
_logger.LogInformation("Sesion GlobalLeaks guardada para {Username}. Generando JWT.", username);
|
||||
|
||||
var expiresAtUtc = DateTimeOffset.UtcNow.AddMinutes(Math.Max(5, _jwtOptions.ExpirationMinutes));
|
||||
|
||||
@@ -12,10 +12,14 @@ namespace ApiDenuncias.Controllers;
|
||||
public sealed class ConfigurationController : ControllerBase
|
||||
{
|
||||
private readonly AppConfigurationService _configurationService;
|
||||
private readonly WorkGroupAdministrationService _workGroupService;
|
||||
|
||||
public ConfigurationController(AppConfigurationService configurationService)
|
||||
public ConfigurationController(
|
||||
AppConfigurationService configurationService,
|
||||
WorkGroupAdministrationService workGroupService)
|
||||
{
|
||||
_configurationService = configurationService;
|
||||
_workGroupService = workGroupService;
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
@@ -25,6 +29,7 @@ public sealed class ConfigurationController : ControllerBase
|
||||
}
|
||||
|
||||
[HttpPut("external-update-cutoff")]
|
||||
[Authorize(Policy = "ConfigurationAdministrators")]
|
||||
public async Task<ActionResult<AppConfigurationDto>> SetExternalUpdateCutoff(
|
||||
UpdateExternalUpdateCutoffRequest request,
|
||||
CancellationToken cancellationToken)
|
||||
@@ -47,4 +52,46 @@ public sealed class ConfigurationController : ControllerBase
|
||||
|
||||
return Ok(await _configurationService.SetExternalUpdateCutoffDateAsync(date, cancellationToken));
|
||||
}
|
||||
|
||||
[HttpGet("work-groups")]
|
||||
[Authorize(Policy = "ConfigurationAdministrators")]
|
||||
public async Task<ActionResult<WorkGroupAdministrationDto>> GetWorkGroups(
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
return Ok(await _workGroupService.GetAsync(cancellationToken));
|
||||
}
|
||||
|
||||
[HttpGet("work-groups/current")]
|
||||
public async Task<ActionResult<CurrentUserWorkGroupsDto>> GetCurrentUserWorkGroups(
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var username = User.Identity?.Name ??
|
||||
throw new InvalidOperationException("No hay usuario autenticado.");
|
||||
|
||||
return Ok(await _workGroupService.GetUserGroupsAsync(username, cancellationToken));
|
||||
}
|
||||
|
||||
[HttpPut("work-groups/users/{username}")]
|
||||
[Authorize(Policy = "ConfigurationAdministrators")]
|
||||
public async Task<ActionResult<WorkGroupAdministrationDto>> SetUserWorkGroups(
|
||||
string username,
|
||||
UpdateUserWorkGroupsRequest request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var changedBy = User.Identity?.Name ??
|
||||
throw new InvalidOperationException("No hay usuario autenticado.");
|
||||
|
||||
try
|
||||
{
|
||||
return Ok(await _workGroupService.UpdateUserGroupsAsync(
|
||||
username,
|
||||
request.GroupCodes ?? [],
|
||||
changedBy,
|
||||
cancellationToken));
|
||||
}
|
||||
catch (InvalidOperationException ex)
|
||||
{
|
||||
return BadRequest(new ApiError(ex.Message));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
using System.Globalization;
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
using ApiDenuncias.Services;
|
||||
using GestionaDenuncias.Shared.Models;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
@@ -13,15 +16,21 @@ public sealed class DenunciasController : ControllerBase
|
||||
private readonly IDenunciaStore _denunciaStore;
|
||||
private readonly IFilteredDenunciaStore _filteredDenunciaStore;
|
||||
private readonly UserComplaintAccessService _accessService;
|
||||
private readonly IInboxTrackingService _trackingService;
|
||||
private readonly ILogger<DenunciasController> _logger;
|
||||
|
||||
public DenunciasController(
|
||||
IDenunciaStore denunciaStore,
|
||||
IFilteredDenunciaStore filteredDenunciaStore,
|
||||
UserComplaintAccessService accessService)
|
||||
UserComplaintAccessService accessService,
|
||||
IInboxTrackingService trackingService,
|
||||
ILogger<DenunciasController> logger)
|
||||
{
|
||||
_denunciaStore = denunciaStore;
|
||||
_filteredDenunciaStore = filteredDenunciaStore;
|
||||
_accessService = accessService;
|
||||
_trackingService = trackingService;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
[HttpPost("schema/ensure")]
|
||||
@@ -36,13 +45,22 @@ public sealed class DenunciasController : ControllerBase
|
||||
[FromQuery] DenunciaListScope scope,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var allowedIds = await GetAllowedIdsAsync(cancellationToken);
|
||||
if (allowedIds.Count == 0)
|
||||
await _denunciaStore.EnsureSchemaAsync(cancellationToken);
|
||||
var access = await _accessService.GetComplaintAccessAsync(
|
||||
GetUsername(),
|
||||
null,
|
||||
cancellationToken);
|
||||
if (access.Count == 0)
|
||||
{
|
||||
return Ok(new List<DenunciasGestiona>());
|
||||
}
|
||||
|
||||
return Ok(await _filteredDenunciaStore.GetDenunciasByIdsAsync(allowedIds, scope, cancellationToken));
|
||||
var complaints = await _filteredDenunciaStore.GetDenunciasByIdsAsync(
|
||||
access.Keys.ToArray(),
|
||||
scope,
|
||||
cancellationToken);
|
||||
ApplyAccessMetadata(complaints, access);
|
||||
return Ok(complaints);
|
||||
}
|
||||
|
||||
[HttpGet("{denunciaId:int}")]
|
||||
@@ -53,7 +71,79 @@ public sealed class DenunciasController : ControllerBase
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
return Ok(await _denunciaStore.GetDenunciaByIdAsync(denunciaId, cancellationToken));
|
||||
var complaint = await _denunciaStore.GetDenunciaByIdAsync(denunciaId, cancellationToken);
|
||||
if (complaint is null)
|
||||
{
|
||||
return Ok(null);
|
||||
}
|
||||
|
||||
var access = await _accessService.GetComplaintAccessAsync(
|
||||
GetUsername(),
|
||||
[denunciaId],
|
||||
cancellationToken);
|
||||
ApplyAccessMetadata([complaint], access);
|
||||
return Ok(complaint);
|
||||
}
|
||||
|
||||
[AllowAnonymous]
|
||||
[HttpGet("{denunciaId:int}/gestiona-fields")]
|
||||
public async Task<ActionResult<GestionaExternalFieldsResponse>> GetGestionaFields(
|
||||
int denunciaId,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
if (denunciaId <= 0)
|
||||
{
|
||||
return BadRequest(new ApiError("Debes indicar un numero de denuncia valido."));
|
||||
}
|
||||
|
||||
var denuncia = await _denunciaStore.GetDenunciaByIdAsync(denunciaId, cancellationToken);
|
||||
if (denuncia is null)
|
||||
{
|
||||
return NotFound(new ApiError("No se ha encontrado la denuncia solicitada."));
|
||||
}
|
||||
|
||||
return BuildGestionaFieldsResponse(denuncia);
|
||||
}
|
||||
|
||||
[AllowAnonymous]
|
||||
[HttpGet("{numeroExpediente:int}/{anioExpediente:int}/gestiona-fields")]
|
||||
public async Task<ActionResult<GestionaExternalFieldsResponse>> GetGestionaFieldsByExpediente(
|
||||
int numeroExpediente,
|
||||
int anioExpediente,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
if (numeroExpediente <= 0 || anioExpediente <= 0)
|
||||
{
|
||||
return BadRequest(new ApiError("Debes indicar un numero de expediente de Gestiona valido."));
|
||||
}
|
||||
|
||||
var expedienteGestiona = $"{numeroExpediente.ToString(CultureInfo.InvariantCulture)}/{anioExpediente.ToString(CultureInfo.InvariantCulture)}";
|
||||
var denuncia = await _filteredDenunciaStore.GetDenunciaByGestionaFileCodeAsync(
|
||||
expedienteGestiona,
|
||||
cancellationToken);
|
||||
if (denuncia is null)
|
||||
{
|
||||
return NotFound(new ApiError("No se ha encontrado una denuncia asociada al expediente de Gestiona solicitado."));
|
||||
}
|
||||
|
||||
return BuildGestionaFieldsResponse(denuncia);
|
||||
}
|
||||
|
||||
private ActionResult<GestionaExternalFieldsResponse> BuildGestionaFieldsResponse(DenunciasGestiona denuncia)
|
||||
{
|
||||
var response = ToGestionaComplaintFields(denuncia);
|
||||
var missingFields = GetMissingGestionaFields(response);
|
||||
if (missingFields.Count > 0)
|
||||
{
|
||||
_logger.LogInformation(
|
||||
"Campos Gestiona denuncia {DenunciaId}: campos vacios={MissingFields}; rawReportLength={RawReportLength}; formFieldsCount={FormFieldsCount}.",
|
||||
denuncia.Id_Denuncia,
|
||||
string.Join(", ", missingFields),
|
||||
denuncia.TextoOriginalReport?.Length ?? 0,
|
||||
denuncia.GetCamposFormulario().Count);
|
||||
}
|
||||
|
||||
return Ok(ToExternalGestionaFields(response));
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
@@ -65,6 +155,7 @@ public sealed class DenunciasController : ControllerBase
|
||||
}
|
||||
|
||||
await _denunciaStore.UpsertDenunciaAsync(denuncia, cancellationToken);
|
||||
await TryRegisterGestionaHistoryFromComplaintAsync(denuncia, cancellationToken);
|
||||
return Ok(new { ok = true });
|
||||
}
|
||||
|
||||
@@ -91,6 +182,32 @@ public sealed class DenunciasController : ControllerBase
|
||||
return Ok(await _denunciaStore.GetFicherosByDenunciaAsync(denunciaId, cancellationToken));
|
||||
}
|
||||
|
||||
[HttpGet("gestiona-history")]
|
||||
public async Task<ActionResult<List<GestionaUploadHistoryEntry>>> GetGestionaHistory(CancellationToken cancellationToken)
|
||||
{
|
||||
await _denunciaStore.EnsureSchemaAsync(cancellationToken);
|
||||
return Ok(await _denunciaStore.GetGestionaUploadHistoryAsync(cancellationToken));
|
||||
}
|
||||
|
||||
[HttpPost("gestiona-history")]
|
||||
public async Task<IActionResult> AddGestionaHistory(
|
||||
GestionaUploadHistoryCreateRequest request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
if (!await CanAccessAsync(request.DenunciaId, cancellationToken))
|
||||
{
|
||||
return Forbid();
|
||||
}
|
||||
|
||||
await _denunciaStore.AddGestionaUploadHistoryAsync(request, GetUsername(), cancellationToken);
|
||||
await _trackingService.MarkReportHandledInGestionaAsync(
|
||||
GetUsername(),
|
||||
request.DenunciaId,
|
||||
request.UploadedAtUtc,
|
||||
cancellationToken);
|
||||
return Ok(new { ok = true });
|
||||
}
|
||||
|
||||
[HttpGet("{denunciaId:int}/ficheros/content")]
|
||||
public async Task<IActionResult> GetFicheroContent(
|
||||
int denunciaId,
|
||||
@@ -178,6 +295,598 @@ public sealed class DenunciasController : ControllerBase
|
||||
private string GetUsername()
|
||||
=> User.Identity?.Name ?? throw new InvalidOperationException("No hay usuario autenticado.");
|
||||
|
||||
private static void ApplyAccessMetadata(
|
||||
IEnumerable<DenunciasGestiona> complaints,
|
||||
IReadOnlyDictionary<int, ComplaintAccessInfo> access)
|
||||
{
|
||||
foreach (var complaint in complaints)
|
||||
{
|
||||
if (!access.TryGetValue(complaint.Id_Denuncia, out var item))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
complaint.OwnerUsername = item.OwnerUsername;
|
||||
complaint.OwnedByCurrentUser = item.OwnedByCurrentUser;
|
||||
complaint.RequiresOwnerConfirmation = item.RequiresOwnerConfirmation;
|
||||
complaint.OwnerWorkGroups = item.OwnerGroupCodes;
|
||||
}
|
||||
}
|
||||
|
||||
private static GestionaComplaintFieldsResponse ToGestionaComplaintFields(DenunciasGestiona denuncia)
|
||||
{
|
||||
var preferenciaNotificacion = ResolveNotificationPreference(denuncia);
|
||||
var seguimiento = ResolveSeguimientoDenuncia(denuncia);
|
||||
var sms = ResolveSmsNotification(denuncia);
|
||||
|
||||
return new GestionaComplaintFieldsResponse(
|
||||
FechaDenuncia: ToNullableDate(denuncia.Fecha),
|
||||
NumeroDenunciaCanal: denuncia.Id_Denuncia,
|
||||
AQuienDenuncia: ResolveReportField(denuncia, denuncia.A_Quien_Denuncia, "a quien denuncia"),
|
||||
ResumenDenuncia: FirstNonEmpty(
|
||||
ResolveReportField(denuncia, string.Empty, "resumen de la denuncia", "resumen denuncia"),
|
||||
ResolveReportField(denuncia, denuncia.Descripcion_Denuncia, "describa su denuncia", "descripcion de la denuncia")),
|
||||
FechaHechos: ResolveFechaHechos(denuncia),
|
||||
LugarHechos: ResolveLugarHechos(denuncia),
|
||||
AmbitoCompetencias: ResolveAmbitoCompetencias(denuncia),
|
||||
SolicitaProteccion: ResolveReportField(denuncia, denuncia.SolicitaProteccion, "solicita medidas concretas de proteccion", "solicita proteccion"),
|
||||
SexoDenunciante: ResolveReportField(denuncia, denuncia.Sexo, "sexo"),
|
||||
AutorizaRemisionDenuncia: ResolveReportField(denuncia, denuncia.AutorizaRemision, "autorizacion para remitir su denuncia", "autoriza remision de la denuncia"),
|
||||
AutorizaNotificacionesViaSms: sms,
|
||||
PreferenciaNotificacionSeguimientoDenuncia: JoinDistinct(preferenciaNotificacion, seguimiento));
|
||||
}
|
||||
|
||||
private static GestionaExternalFieldsResponse ToExternalGestionaFields(GestionaComplaintFieldsResponse source)
|
||||
{
|
||||
return new GestionaExternalFieldsResponse(
|
||||
new Dictionary<string, GestionaExternalFieldValue>(StringComparer.Ordinal)
|
||||
{
|
||||
["fechaDenuncia"] = StringField(source.FechaDenuncia?.ToString("O", CultureInfo.InvariantCulture) ?? string.Empty),
|
||||
["numeroDenunciaCanal"] = StringField(source.NumeroDenunciaCanal.ToString(CultureInfo.InvariantCulture)),
|
||||
["aQuienDenuncia"] = StringField(source.AQuienDenuncia),
|
||||
["resumenDenuncia"] = StringField(source.ResumenDenuncia),
|
||||
["fechaHechos"] = StringField(source.FechaHechos),
|
||||
["lugarHechos"] = StringField(source.LugarHechos),
|
||||
["ambitoCompetencias"] = StringField(source.AmbitoCompetencias),
|
||||
["solicitaProteccion"] = StringField(source.SolicitaProteccion),
|
||||
["sexoDenunciante"] = StringField(source.SexoDenunciante),
|
||||
["autorizaRemisionDenuncia"] = StringField(source.AutorizaRemisionDenuncia),
|
||||
["autorizaNotificacionesViaSms"] = StringField(source.AutorizaNotificacionesViaSms),
|
||||
["preferenciaNotificacionSeguimientoDenuncia"] = StringField(source.PreferenciaNotificacionSeguimientoDenuncia)
|
||||
});
|
||||
}
|
||||
|
||||
private static GestionaExternalFieldValue StringField(string? value)
|
||||
=> new("STRING", value?.Trim() ?? string.Empty);
|
||||
|
||||
private async Task TryRegisterGestionaHistoryFromComplaintAsync(
|
||||
DenunciasGestiona denuncia,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
if (!denuncia.EnGestiona || denuncia.FechaSubidaAGestiona == DateTime.MinValue)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
await _denunciaStore.AddGestionaUploadHistoryAsync(
|
||||
new GestionaUploadHistoryCreateRequest(
|
||||
denuncia.Id_Denuncia,
|
||||
denuncia.Expediente_Gestiona ?? string.Empty,
|
||||
denuncia.ExpedienteGestionaMostrable,
|
||||
denuncia.UltimaSubidaGestionaTipoMostrable,
|
||||
denuncia.UltimoGrupoAsignadoGestionaMostrable,
|
||||
denuncia.FechaSubidaAGestiona,
|
||||
denuncia.NombreDenuncia ?? string.Empty,
|
||||
denuncia.ArchivoElegido ?? string.Empty),
|
||||
GetUsername(),
|
||||
cancellationToken);
|
||||
await _trackingService.MarkReportHandledInGestionaAsync(
|
||||
GetUsername(),
|
||||
denuncia.Id_Denuncia,
|
||||
denuncia.FechaSubidaAGestiona,
|
||||
cancellationToken);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogWarning(
|
||||
ex,
|
||||
"No se ha podido registrar el historico de subida a Gestiona para la denuncia {DenunciaId}.",
|
||||
denuncia.Id_Denuncia);
|
||||
}
|
||||
}
|
||||
|
||||
private static DateTime? ToNullableDate(DateTime value)
|
||||
=> value == DateTime.MinValue ? null : value;
|
||||
|
||||
private static IReadOnlyList<string> GetMissingGestionaFields(GestionaComplaintFieldsResponse response)
|
||||
{
|
||||
var missing = new List<string>();
|
||||
|
||||
AddIfMissing(missing, response.AQuienDenuncia, nameof(response.AQuienDenuncia));
|
||||
AddIfMissing(missing, response.ResumenDenuncia, nameof(response.ResumenDenuncia));
|
||||
AddIfMissing(missing, response.FechaHechos, nameof(response.FechaHechos));
|
||||
AddIfMissing(missing, response.LugarHechos, nameof(response.LugarHechos));
|
||||
AddIfMissing(missing, response.AmbitoCompetencias, nameof(response.AmbitoCompetencias));
|
||||
AddIfMissing(missing, response.SolicitaProteccion, nameof(response.SolicitaProteccion));
|
||||
AddIfMissing(missing, response.SexoDenunciante, nameof(response.SexoDenunciante));
|
||||
AddIfMissing(missing, response.AutorizaRemisionDenuncia, nameof(response.AutorizaRemisionDenuncia));
|
||||
AddIfMissing(missing, response.AutorizaNotificacionesViaSms, nameof(response.AutorizaNotificacionesViaSms));
|
||||
AddIfMissing(missing, response.PreferenciaNotificacionSeguimientoDenuncia, nameof(response.PreferenciaNotificacionSeguimientoDenuncia));
|
||||
|
||||
return missing;
|
||||
}
|
||||
|
||||
private static void AddIfMissing(List<string> missing, string? value, string fieldName)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(value))
|
||||
{
|
||||
missing.Add(fieldName);
|
||||
}
|
||||
}
|
||||
|
||||
private static string ResolveFechaHechos(DenunciasGestiona denuncia)
|
||||
{
|
||||
var literalValue = ResolveReportField(
|
||||
denuncia,
|
||||
string.Empty,
|
||||
"fecha de los hechos que denuncia",
|
||||
"fecha de los hechos");
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(literalValue))
|
||||
{
|
||||
return literalValue;
|
||||
}
|
||||
|
||||
var textDate = ExtractDateAfterReportLabel(
|
||||
denuncia.TextoOriginalReport,
|
||||
normalizedLabel => normalizedLabel.Contains("fecha", StringComparison.Ordinal) &&
|
||||
normalizedLabel.Contains("hechos", StringComparison.Ordinal));
|
||||
if (!string.IsNullOrWhiteSpace(textDate))
|
||||
{
|
||||
return textDate;
|
||||
}
|
||||
|
||||
return denuncia.Fecha_Hechos == DateTime.MinValue
|
||||
? string.Empty
|
||||
: denuncia.Fecha_Hechos.ToString("dd/MM/yyyy", CultureInfo.InvariantCulture);
|
||||
}
|
||||
|
||||
private static string ResolveLugarHechos(DenunciasGestiona denuncia)
|
||||
{
|
||||
var directValue = ResolveReportField(
|
||||
denuncia,
|
||||
denuncia.Lugar_Hechos,
|
||||
"lugar en el que ocurrieron los hechos que denuncia",
|
||||
"lugar en la que ocurrieron los hechos que denuncia",
|
||||
"lugar de los hechos");
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(directValue))
|
||||
{
|
||||
return directValue;
|
||||
}
|
||||
|
||||
return ExtractReportTextValue(
|
||||
denuncia.TextoOriginalReport,
|
||||
normalizedLabel => normalizedLabel.Contains("lugar", StringComparison.Ordinal) &&
|
||||
normalizedLabel.Contains("hechos", StringComparison.Ordinal));
|
||||
}
|
||||
|
||||
private static string ResolveSeguimientoDenuncia(DenunciasGestiona denuncia)
|
||||
{
|
||||
var directValue = ResolveReportField(
|
||||
denuncia,
|
||||
denuncia.SeguimientoOnline,
|
||||
"seguimiento online",
|
||||
"seguimiento de su denuncia");
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(directValue))
|
||||
{
|
||||
return directValue;
|
||||
}
|
||||
|
||||
return ReportContainsLabel(denuncia.TextoOriginalReport, "seguimiento online")
|
||||
? "Seguimiento Online"
|
||||
: string.Empty;
|
||||
}
|
||||
|
||||
private static string ResolveNotificationPreference(DenunciasGestiona denuncia)
|
||||
{
|
||||
var directValue = ResolveReportField(
|
||||
denuncia,
|
||||
denuncia.Notificacion_Preferencia,
|
||||
"preferencia de notificacion",
|
||||
"seleccione su preferencia de notificacion y seguimiento de su denuncia",
|
||||
"preferencia de notificacion y seguimiento de su denuncia",
|
||||
"notificaciones");
|
||||
if (!string.IsNullOrWhiteSpace(directValue))
|
||||
{
|
||||
return directValue;
|
||||
}
|
||||
|
||||
directValue = ResolveReportFieldAllowingLabelValues(
|
||||
denuncia,
|
||||
string.Empty,
|
||||
"preferencia de notificacion",
|
||||
"seleccione su preferencia de notificacion y seguimiento de su denuncia",
|
||||
"preferencia de notificacion y seguimiento de su denuncia");
|
||||
if (!string.IsNullOrWhiteSpace(directValue))
|
||||
{
|
||||
return directValue;
|
||||
}
|
||||
|
||||
var electronic = ResolveReportField(
|
||||
denuncia,
|
||||
denuncia.Notificacion_Electronica,
|
||||
"notificaciones electronicas",
|
||||
"notificacion electronica");
|
||||
if (!string.IsNullOrWhiteSpace(electronic) ||
|
||||
ReportContainsLabel(denuncia.TextoOriginalReport, "notificaciones electronicas"))
|
||||
{
|
||||
return "Notificaciones electronicas";
|
||||
}
|
||||
|
||||
var postal = ResolveReportField(
|
||||
denuncia,
|
||||
denuncia.NotificacionPostal,
|
||||
"autorizo recibir notificaciones via correo postal",
|
||||
"notificaciones via correo postal",
|
||||
"correo postal");
|
||||
return string.IsNullOrWhiteSpace(postal) ? string.Empty : "Correo postal";
|
||||
}
|
||||
|
||||
private static string ResolveSmsNotification(DenunciasGestiona denuncia)
|
||||
{
|
||||
return ResolveReportField(
|
||||
denuncia,
|
||||
denuncia.Notificacion_Sms,
|
||||
"autorizo recibir notificaciones via sms",
|
||||
"autorizacion notificaciones via sms",
|
||||
"autorizacion para recibir notificaciones via sms",
|
||||
"autoriza notificaciones via sms",
|
||||
"autorizo recibir notificaciones sms",
|
||||
"notificaciones via sms",
|
||||
"sms");
|
||||
}
|
||||
|
||||
private static string ResolveAmbitoCompetencias(DenunciasGestiona denuncia)
|
||||
{
|
||||
var directValue = ResolveReportField(
|
||||
denuncia,
|
||||
FirstNonEmpty(denuncia.Modalidad_Informacion, denuncia.Asunto),
|
||||
"asunto",
|
||||
"categoria",
|
||||
"tipo de denuncia",
|
||||
"ambito de competencias",
|
||||
"ambito competencial",
|
||||
"ambito",
|
||||
"competencias",
|
||||
"modalidad de informacion",
|
||||
"modalidad informacion");
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(directValue))
|
||||
{
|
||||
return directValue;
|
||||
}
|
||||
|
||||
return FindReportFieldValue(denuncia, normalizedLabel =>
|
||||
normalizedLabel.Contains("ambito", StringComparison.Ordinal) &&
|
||||
normalizedLabel.Contains("compet", StringComparison.Ordinal));
|
||||
}
|
||||
|
||||
private static string ResolveReportField(
|
||||
DenunciasGestiona denuncia,
|
||||
string? currentValue,
|
||||
params string[] candidateLabels)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(currentValue))
|
||||
{
|
||||
return currentValue.Trim();
|
||||
}
|
||||
|
||||
var labels = candidateLabels
|
||||
.Select(NormalizeLabel)
|
||||
.Where(label => !string.IsNullOrWhiteSpace(label))
|
||||
.ToHashSet(StringComparer.Ordinal);
|
||||
|
||||
if (labels.Count == 0)
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
foreach (var field in denuncia.GetCamposFormulario())
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(field.Value) &&
|
||||
labels.Contains(NormalizeLabel(field.Label)))
|
||||
{
|
||||
return field.Value.Trim();
|
||||
}
|
||||
}
|
||||
|
||||
return ExtractReportTextValue(denuncia.TextoOriginalReport, labels);
|
||||
}
|
||||
|
||||
private static string ResolveReportFieldAllowingLabelValues(
|
||||
DenunciasGestiona denuncia,
|
||||
string? currentValue,
|
||||
params string[] candidateLabels)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(currentValue))
|
||||
{
|
||||
return currentValue.Trim();
|
||||
}
|
||||
|
||||
var labels = candidateLabels
|
||||
.Select(NormalizeLabel)
|
||||
.Where(label => !string.IsNullOrWhiteSpace(label))
|
||||
.ToHashSet(StringComparer.Ordinal);
|
||||
|
||||
if (labels.Count == 0)
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
foreach (var field in denuncia.GetCamposFormulario())
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(field.Value) &&
|
||||
labels.Contains(NormalizeLabel(field.Label)))
|
||||
{
|
||||
return field.Value.Trim();
|
||||
}
|
||||
}
|
||||
|
||||
return ExtractReportTextValue(denuncia.TextoOriginalReport, labels, stopAtLikelyLabels: false);
|
||||
}
|
||||
|
||||
private static string FindReportFieldValue(
|
||||
DenunciasGestiona denuncia,
|
||||
Func<string, bool> labelPredicate)
|
||||
{
|
||||
foreach (var field in denuncia.GetCamposFormulario())
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(field.Value) &&
|
||||
labelPredicate(NormalizeLabel(field.Label)))
|
||||
{
|
||||
return field.Value.Trim();
|
||||
}
|
||||
}
|
||||
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
private static string ExtractDateAfterReportLabel(string? reportText, Func<string, bool> labelPredicate)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(reportText))
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
var lines = reportText
|
||||
.Replace("\r\n", "\n", StringComparison.Ordinal)
|
||||
.Replace('\r', '\n')
|
||||
.Split('\n')
|
||||
.Select(line => line.Trim())
|
||||
.ToArray();
|
||||
|
||||
for (var index = 0; index < lines.Length; index++)
|
||||
{
|
||||
var normalized = NormalizeLabel(lines[index]);
|
||||
if (!labelPredicate(normalized))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
for (var valueIndex = index + 1; valueIndex < lines.Length && valueIndex <= index + 12; valueIndex++)
|
||||
{
|
||||
var candidate = lines[valueIndex].Trim();
|
||||
if (string.IsNullOrWhiteSpace(candidate) || IsReportStructuralLine(candidate))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var dateMatch = Regex.Match(
|
||||
candidate,
|
||||
@"\b\d{1,2}/\d{1,2}/\d{4}\b",
|
||||
RegexOptions.CultureInvariant);
|
||||
if (dateMatch.Success)
|
||||
{
|
||||
return dateMatch.Value;
|
||||
}
|
||||
|
||||
if (IsLikelyReportLabel(candidate))
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var rawMatch = Regex.Match(
|
||||
reportText,
|
||||
@"fecha\s+de\s+los\s+hechos[\s\S]{0,600}?(\b\d{1,2}/\d{1,2}/\d{4}\b)",
|
||||
RegexOptions.IgnoreCase | RegexOptions.CultureInvariant);
|
||||
return rawMatch.Success ? rawMatch.Groups[1].Value : string.Empty;
|
||||
}
|
||||
|
||||
private static string ExtractReportTextValue(string? reportText, IReadOnlySet<string> normalizedLabels)
|
||||
=> ExtractReportTextValue(
|
||||
reportText,
|
||||
normalizedLabel => normalizedLabels.Contains(normalizedLabel));
|
||||
|
||||
private static string ExtractReportTextValue(
|
||||
string? reportText,
|
||||
IReadOnlySet<string> normalizedLabels,
|
||||
bool stopAtLikelyLabels)
|
||||
=> ExtractReportTextValue(
|
||||
reportText,
|
||||
normalizedLabel => normalizedLabels.Contains(normalizedLabel),
|
||||
stopAtLikelyLabels);
|
||||
|
||||
private static string ExtractReportTextValue(string? reportText, Func<string, bool> labelPredicate)
|
||||
=> ExtractReportTextValue(reportText, labelPredicate, stopAtLikelyLabels: true);
|
||||
|
||||
private static string ExtractReportTextValue(
|
||||
string? reportText,
|
||||
Func<string, bool> labelPredicate,
|
||||
bool stopAtLikelyLabels)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(reportText))
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
var lines = reportText
|
||||
.Replace("\r\n", "\n", StringComparison.Ordinal)
|
||||
.Replace('\r', '\n')
|
||||
.Split('\n')
|
||||
.Select(line => line.Trim())
|
||||
.ToArray();
|
||||
|
||||
for (var index = 0; index < lines.Length; index++)
|
||||
{
|
||||
if (!labelPredicate(NormalizeLabel(lines[index])))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
for (var valueIndex = index + 1; valueIndex < lines.Length; valueIndex++)
|
||||
{
|
||||
var candidate = lines[valueIndex].Trim();
|
||||
if (string.IsNullOrWhiteSpace(candidate) || IsReportStructuralLine(candidate))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (stopAtLikelyLabels && IsLikelyReportLabel(candidate))
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
return candidate;
|
||||
}
|
||||
}
|
||||
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
private static bool ReportContainsLabel(string? reportText, string label)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(reportText))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var normalizedLabel = NormalizeLabel(label);
|
||||
return reportText
|
||||
.Replace("\r\n", "\n", StringComparison.Ordinal)
|
||||
.Replace('\r', '\n')
|
||||
.Split('\n')
|
||||
.Any(line => string.Equals(NormalizeLabel(line.Trim()), normalizedLabel, StringComparison.Ordinal));
|
||||
}
|
||||
|
||||
private static bool IsReportStructuralLine(string value)
|
||||
{
|
||||
if (Regex.IsMatch(value, @"^\d+/\d+$", RegexOptions.CultureInvariant))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
return value.StartsWith("REPORT ", StringComparison.OrdinalIgnoreCase) ||
|
||||
value.Equals("[CONFIDENTIAL]", StringComparison.OrdinalIgnoreCase) ||
|
||||
value.Equals("{Messages}", StringComparison.OrdinalIgnoreCase) ||
|
||||
value.StartsWith("Comments", StringComparison.OrdinalIgnoreCase) ||
|
||||
value.StartsWith("De:", StringComparison.OrdinalIgnoreCase) ||
|
||||
value.StartsWith("Fecha:", StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
private static bool IsLikelyReportLabel(string value)
|
||||
{
|
||||
var normalized = NormalizeLabel(value);
|
||||
if (string.IsNullOrWhiteSpace(normalized))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return normalized is "descripcion"
|
||||
or "datos del denunciante"
|
||||
or "condiciones y reglas de uso"
|
||||
or "tratamiento de datos personales"
|
||||
or "preferencias de notificacion"
|
||||
or "notificaciones electronicas" ||
|
||||
normalized.StartsWith("indique ", StringComparison.Ordinal) ||
|
||||
normalized.StartsWith("describa ", StringComparison.Ordinal) ||
|
||||
normalized.StartsWith("autoriza ", StringComparison.Ordinal) ||
|
||||
normalized.StartsWith("autorizacion ", StringComparison.Ordinal) ||
|
||||
normalized.StartsWith("ha denunciado ", StringComparison.Ordinal) ||
|
||||
normalized.StartsWith("solicita ", StringComparison.Ordinal) ||
|
||||
normalized.StartsWith("por favor ", StringComparison.Ordinal) ||
|
||||
normalized.Contains(" denuncia", StringComparison.Ordinal) ||
|
||||
normalized.Contains(" hechos", StringComparison.Ordinal) ||
|
||||
normalized.Contains(" notificacion", StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
private static string JoinDistinct(params string[] values)
|
||||
{
|
||||
var result = new List<string>();
|
||||
var seen = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||
foreach (var value in values)
|
||||
{
|
||||
var trimmed = value?.Trim();
|
||||
if (!string.IsNullOrWhiteSpace(trimmed) && seen.Add(trimmed))
|
||||
{
|
||||
result.Add(trimmed);
|
||||
}
|
||||
}
|
||||
|
||||
return string.Join("; ", result);
|
||||
}
|
||||
|
||||
private static string FirstNonEmpty(params string[] values)
|
||||
{
|
||||
foreach (var value in values)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(value))
|
||||
{
|
||||
return value.Trim();
|
||||
}
|
||||
}
|
||||
|
||||
return string.Empty;
|
||||
}
|
||||
//pruabass
|
||||
private static string NormalizeLabel(string? value)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(value))
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
var normalized = value.Normalize(NormalizationForm.FormD);
|
||||
var builder = new StringBuilder(normalized.Length);
|
||||
|
||||
foreach (var ch in normalized)
|
||||
{
|
||||
if (CharUnicodeInfo.GetUnicodeCategory(ch) == UnicodeCategory.NonSpacingMark)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (ch is '\u00BA' or '\u00AA')
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
builder.Append(char.IsLetterOrDigit(ch) ? char.ToLowerInvariant(ch) : ' ');
|
||||
}
|
||||
|
||||
return string.Join(
|
||||
' ',
|
||||
builder
|
||||
.ToString()
|
||||
.Normalize(NormalizationForm.FormC)
|
||||
.Split(' ', StringSplitOptions.RemoveEmptyEntries));
|
||||
}
|
||||
|
||||
private static string GetAttachmentContentType(string? fileName)
|
||||
{
|
||||
return Path.GetExtension(fileName ?? string.Empty).ToLowerInvariant() switch
|
||||
|
||||
@@ -29,7 +29,6 @@ public sealed class GestionaController : ControllerBase
|
||||
try
|
||||
{
|
||||
var file = await _gestiona.CreateFileAsync(
|
||||
request.ProcedureId,
|
||||
request.Subject,
|
||||
request.DocumentSeries,
|
||||
request.SiaCode);
|
||||
@@ -52,11 +51,28 @@ public sealed class GestionaController : ControllerBase
|
||||
await _gestiona.OpenFileAsync(
|
||||
request.FileUrl,
|
||||
request.FileOpenUrl,
|
||||
request.ManagementUnitGroupId,
|
||||
request.AssignedGroupId,
|
||||
request.AssignedGroupCode,
|
||||
request.Confidential,
|
||||
request.FreeTitle,
|
||||
request.SiaCode);
|
||||
request.FreeTitle);
|
||||
|
||||
return Ok(new { ok = true });
|
||||
}
|
||||
catch (InvalidOperationException ex)
|
||||
{
|
||||
return BadRequest(new ApiError(ex.Message));
|
||||
}
|
||||
}
|
||||
|
||||
[HttpPost("files/assignees")]
|
||||
public async Task<IActionResult> AssignFile(
|
||||
GestionaAssignFileRequest request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
try
|
||||
{
|
||||
await _gestiona.AssignFileAsync(
|
||||
request.FileUrl,
|
||||
request.AssignedGroupCode);
|
||||
|
||||
return Ok(new { ok = true });
|
||||
}
|
||||
@@ -86,6 +102,26 @@ public sealed class GestionaController : ControllerBase
|
||||
}
|
||||
}
|
||||
|
||||
[HttpGet("files/audit/latest")]
|
||||
public async Task<ActionResult<GestionaAuditInfo?>> GetLatestFileAudit(
|
||||
[FromQuery] string fileUrl,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(fileUrl))
|
||||
{
|
||||
return BadRequest(new ApiError("Debes indicar la URL del expediente de Gestiona."));
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
return Ok(await _gestiona.ObtenerUltimaAuditoriaExpedienteAsync(fileUrl));
|
||||
}
|
||||
catch (InvalidOperationException ex)
|
||||
{
|
||||
return BadRequest(new ApiError(ex.Message));
|
||||
}
|
||||
}
|
||||
|
||||
[HttpPost("thirds/ensure-link")]
|
||||
public async Task<IActionResult> EnsureThirdAndLink(
|
||||
GestionaEnsureThirdRequest request,
|
||||
@@ -98,8 +134,7 @@ public sealed class GestionaController : ControllerBase
|
||||
|
||||
try
|
||||
{
|
||||
await _gestiona.AsegurarTerceroYEnlazarAsync(request.FileUrl, request.ThirdParty);
|
||||
return Ok(new { ok = true });
|
||||
return Ok(await _gestiona.AsegurarTerceroYEnlazarAsync(request.FileUrl, request.ThirdParty));
|
||||
}
|
||||
catch (ArgumentException ex)
|
||||
{
|
||||
@@ -157,8 +192,10 @@ public sealed class GestionaController : ControllerBase
|
||||
{
|
||||
await _workflow.TramitarDocumentoAsync(
|
||||
request.DocumentUrl,
|
||||
request.AssignedGroupHref,
|
||||
request.ComplaintId);
|
||||
request.AssignedGroupCode,
|
||||
request.ComplaintId,
|
||||
request.IsUpdate,
|
||||
request.UpdateSource);
|
||||
|
||||
return Ok(new { ok = true });
|
||||
}
|
||||
|
||||
@@ -10,9 +10,17 @@ namespace ApiDenuncias.Controllers;
|
||||
[Route("api/inbox")]
|
||||
public sealed class InboxController : ControllerBase
|
||||
{
|
||||
private static readonly TimeSpan[] ExportRetryDelays =
|
||||
[
|
||||
TimeSpan.FromSeconds(2),
|
||||
TimeSpan.FromSeconds(5),
|
||||
TimeSpan.FromSeconds(10)
|
||||
];
|
||||
|
||||
private readonly GlobalLeaksSessionStore _sessionStore;
|
||||
private readonly PendingGlobalLeaksLoginStore _pendingLoginStore;
|
||||
private readonly GlobalLeaksClient _globalLeaksClient;
|
||||
private readonly GlobalLeaksSessionKeepAliveService _sessionKeepAliveService;
|
||||
private readonly DenunciaInboxService _inboxService;
|
||||
private readonly IInboxTrackingService _trackingService;
|
||||
private readonly ILogger<InboxController> _logger;
|
||||
@@ -21,6 +29,7 @@ public sealed class InboxController : ControllerBase
|
||||
GlobalLeaksSessionStore sessionStore,
|
||||
PendingGlobalLeaksLoginStore pendingLoginStore,
|
||||
GlobalLeaksClient globalLeaksClient,
|
||||
GlobalLeaksSessionKeepAliveService sessionKeepAliveService,
|
||||
DenunciaInboxService inboxService,
|
||||
IInboxTrackingService trackingService,
|
||||
ILogger<InboxController> logger)
|
||||
@@ -28,6 +37,7 @@ public sealed class InboxController : ControllerBase
|
||||
_sessionStore = sessionStore;
|
||||
_pendingLoginStore = pendingLoginStore;
|
||||
_globalLeaksClient = globalLeaksClient;
|
||||
_sessionKeepAliveService = sessionKeepAliveService;
|
||||
_inboxService = inboxService;
|
||||
_trackingService = trackingService;
|
||||
_logger = logger;
|
||||
@@ -74,7 +84,7 @@ public sealed class InboxController : ControllerBase
|
||||
_logger.LogError(ex, "No se ha podido preparar la renovacion GlobalLeaks para {Username}.", username);
|
||||
return StatusCode(
|
||||
StatusCodes.Status500InternalServerError,
|
||||
new ApiError($"No se ha podido preparar la renovacion: {ex.GetType().Name}: {ex.Message}"));
|
||||
new ApiError("No se ha podido preparar la renovacion de GlobalLeaks. Intentalo de nuevo en unos segundos."));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -124,7 +134,14 @@ public sealed class InboxController : ControllerBase
|
||||
cancellationToken);
|
||||
}
|
||||
|
||||
await _sessionStore.UpdateSessionAsync(username, session.Id, session.Role, cancellationToken);
|
||||
await _sessionStore.UpdateSessionAsync(
|
||||
username,
|
||||
session.Id,
|
||||
session.Role,
|
||||
session.DpopPrivateKey,
|
||||
session.ProofOfWorkToken,
|
||||
session.SessionExpiresAtUtc,
|
||||
cancellationToken);
|
||||
var stored = await _sessionStore.GetAsync(username, cancellationToken);
|
||||
return Ok(ToDto(stored));
|
||||
}
|
||||
@@ -141,7 +158,46 @@ public sealed class InboxController : ControllerBase
|
||||
_logger.LogError(ex, "No se ha podido cargar la bandeja GlobalLeaks para {Username}.", username);
|
||||
return StatusCode(
|
||||
StatusCodes.Status500InternalServerError,
|
||||
new ApiError($"No se ha podido cargar la bandeja: {ex.GetType().Name}: {ex.Message}"));
|
||||
new ApiError("No se ha podido renovar la sesion de GlobalLeaks. Intentalo de nuevo en unos segundos."));
|
||||
}
|
||||
}
|
||||
|
||||
[HttpPost("session/keepalive")]
|
||||
public async Task<ActionResult<ApiGlobalLeaksSessionDto?>> KeepSessionAlive(
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var username = GetUsername();
|
||||
|
||||
try
|
||||
{
|
||||
var session = await _sessionKeepAliveService.KeepAliveAsync(username, cancellationToken);
|
||||
return Ok(ToDto(session));
|
||||
}
|
||||
catch (GlobalLeaksSessionExpiredException)
|
||||
{
|
||||
var session = await _sessionStore.GetAsync(username, cancellationToken);
|
||||
return Ok(ToDto(session));
|
||||
}
|
||||
catch (GlobalLeaksValidationException ex)
|
||||
{
|
||||
_logger.LogWarning(
|
||||
"No se ha podido mantener activa la sesion GlobalLeaks de {Username}. Status={StatusCode}. Mensaje={Message}",
|
||||
username,
|
||||
ex.StatusCode,
|
||||
ex.Message);
|
||||
|
||||
return StatusCode(
|
||||
ex.StatusCode is >= 500 and <= 504
|
||||
? StatusCodes.Status503ServiceUnavailable
|
||||
: StatusCodes.Status502BadGateway,
|
||||
new ApiError("No se ha podido renovar temporalmente la sesion de GlobalLeaks."));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error renovando la sesion GlobalLeaks de {Username}.", username);
|
||||
return StatusCode(
|
||||
StatusCodes.Status503ServiceUnavailable,
|
||||
new ApiError("No se ha podido renovar temporalmente la sesion de GlobalLeaks."));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -164,12 +220,25 @@ public sealed class InboxController : ControllerBase
|
||||
|
||||
try
|
||||
{
|
||||
var contexts = await _globalLeaksClient.GetContextsAsync(session.SessionId!, cancellationToken);
|
||||
var reports = await _globalLeaksClient.GetReportsAsync(session.SessionId!, "all", null, null, cancellationToken, contexts);
|
||||
var enrichedReports = await _trackingService.RegisterSnapshotAsync(username, reports, cancellationToken);
|
||||
var state = await _trackingService.GetUserStateAsync(username, cancellationToken);
|
||||
var contexts = await _globalLeaksClient.GetContextsAsync(session.SessionId!, cancellationToken);
|
||||
var reports = await _globalLeaksClient.GetReportsAsync(
|
||||
session.SessionId!,
|
||||
"all",
|
||||
null,
|
||||
null,
|
||||
cancellationToken,
|
||||
contexts,
|
||||
session.DpopPrivateKey);
|
||||
var enrichedReports = await _trackingService.RegisterSnapshotAsync(username, reports, cancellationToken);
|
||||
var activityReports = await _globalLeaksClient.EnrichReportsWithActivityAsync(
|
||||
session.SessionId!,
|
||||
enrichedReports,
|
||||
state.LastDownloadedReportMomentUtc,
|
||||
cancellationToken,
|
||||
session.DpopPrivateKey);
|
||||
|
||||
return Ok(new InboxSnapshotResponse(contexts, enrichedReports, state));
|
||||
return Ok(new InboxSnapshotResponse(contexts, activityReports, state));
|
||||
}
|
||||
catch (GlobalLeaksSessionExpiredException)
|
||||
{
|
||||
@@ -178,14 +247,14 @@ public sealed class InboxController : ControllerBase
|
||||
}
|
||||
catch (GlobalLeaksValidationException ex)
|
||||
{
|
||||
return StatusCode(ex.StatusCode, new ApiError(ex.Message));
|
||||
return ToGlobalLeaksApiError(ex, "cargar la bandeja de GlobalLeaks");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "No se ha podido cargar la bandeja GlobalLeaks para {Username}.", username);
|
||||
return StatusCode(
|
||||
StatusCodes.Status500InternalServerError,
|
||||
new ApiError($"No se ha podido cargar la bandeja: {ex.GetType().Name}: {ex.Message}"));
|
||||
new ApiError("No se ha podido cargar la bandeja de GlobalLeaks. Intentalo de nuevo en unos segundos."));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -202,27 +271,57 @@ public sealed class InboxController : ControllerBase
|
||||
return Unauthorized(new ApiError("La sesion de GlobalLeaks ha caducado. Renueva el 2FA.", SessionExpired: true));
|
||||
}
|
||||
|
||||
var report = string.IsNullOrWhiteSpace(request.Report.Id)
|
||||
? request.Report with { Id = reportId }
|
||||
: request.Report;
|
||||
var report = request.Report with { Id = reportId };
|
||||
|
||||
try
|
||||
{
|
||||
await _trackingService.EnsureReportCanBeImportedByUserAsync(username, report, cancellationToken);
|
||||
await _trackingService.EnsureReportCanBeImportedByUserAsync(
|
||||
username,
|
||||
report,
|
||||
request.ConfirmDifferentOwner,
|
||||
cancellationToken);
|
||||
|
||||
var zip = await _globalLeaksClient.DownloadReportZipAsync(session.SessionId!, report.Id, cancellationToken);
|
||||
ReportDetailDto? reportDetail = null;
|
||||
try
|
||||
{
|
||||
reportDetail = await _globalLeaksClient.GetReportDetailAsync(
|
||||
session.SessionId!,
|
||||
report.Id,
|
||||
report.LastAccess,
|
||||
cancellationToken,
|
||||
session.DpopPrivateKey);
|
||||
}
|
||||
catch (GlobalLeaksValidationException ex) when (ex.StatusCode == StatusCodes.Status422UnprocessableEntity ||
|
||||
ex.StatusCode is >= 500 and <= 504)
|
||||
{
|
||||
_logger.LogWarning(
|
||||
ex,
|
||||
"No se ha podido obtener el detalle de fechas de adjuntos para la denuncia {ReportId}. Se usara la fecha del paquete exportado.",
|
||||
report.Id);
|
||||
}
|
||||
|
||||
var reportPackage = await DownloadReportPackageWithRetryAsync(session, report, cancellationToken);
|
||||
|
||||
FileDownloadResult? json = null;
|
||||
try
|
||||
{
|
||||
json = await _globalLeaksClient.ExportReportJsonAsync(session.SessionId!, report.Id, cancellationToken);
|
||||
json = await _globalLeaksClient.ExportReportJsonAsync(
|
||||
session.SessionId!,
|
||||
report.Id,
|
||||
cancellationToken,
|
||||
session.DpopPrivateKey);
|
||||
}
|
||||
catch (GlobalLeaksValidationException ex) when (ex.StatusCode == 422)
|
||||
{
|
||||
json = null;
|
||||
}
|
||||
|
||||
var result = await _inboxService.ImportFromGlobalLeaksAsync(zip, json, cancellationToken);
|
||||
var result = await _inboxService.ImportFromGlobalLeaksAsync(
|
||||
reportPackage,
|
||||
json,
|
||||
reportDetail,
|
||||
report,
|
||||
cancellationToken);
|
||||
if (result.ImportedCount > 0)
|
||||
{
|
||||
await _trackingService.MarkReportImportedAsync(
|
||||
@@ -241,14 +340,18 @@ public sealed class InboxController : ControllerBase
|
||||
}
|
||||
catch (GlobalLeaksValidationException ex)
|
||||
{
|
||||
return StatusCode(ex.StatusCode, new ApiError(ex.Message));
|
||||
return ToGlobalLeaksApiError(ex, "importar la denuncia");
|
||||
}
|
||||
catch (ReportOwnershipException ex)
|
||||
{
|
||||
return Conflict(new ApiError(ex.Message));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "No se ha podido importar la denuncia {ReportId} para {Username}.", reportId, username);
|
||||
return StatusCode(
|
||||
StatusCodes.Status500InternalServerError,
|
||||
new ApiError($"No se ha podido importar la denuncia: {ex.GetType().Name}: {ex.Message}"));
|
||||
new ApiError("No se ha podido importar la denuncia. Intentalo de nuevo en unos segundos."));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -267,7 +370,12 @@ public sealed class InboxController : ControllerBase
|
||||
|
||||
try
|
||||
{
|
||||
return Ok(await _globalLeaksClient.GetReportDetailAsync(session.SessionId!, reportId, lastAccess, cancellationToken));
|
||||
return Ok(await _globalLeaksClient.GetReportDetailAsync(
|
||||
session.SessionId!,
|
||||
reportId,
|
||||
lastAccess,
|
||||
cancellationToken,
|
||||
session.DpopPrivateKey));
|
||||
}
|
||||
catch (GlobalLeaksSessionExpiredException)
|
||||
{
|
||||
@@ -276,38 +384,137 @@ public sealed class InboxController : ControllerBase
|
||||
}
|
||||
catch (GlobalLeaksValidationException ex)
|
||||
{
|
||||
return StatusCode(ex.StatusCode, new ApiError(ex.Message));
|
||||
return ToGlobalLeaksApiError(ex, "leer el detalle de la denuncia");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "No se ha podido leer el detalle de la denuncia {ReportId} para {Username}.", reportId, username);
|
||||
return StatusCode(
|
||||
StatusCodes.Status500InternalServerError,
|
||||
new ApiError($"No se ha podido leer el detalle de la denuncia: {ex.GetType().Name}: {ex.Message}"));
|
||||
new ApiError("No se ha podido abrir el detalle de la denuncia. Intentalo de nuevo en unos segundos."));
|
||||
}
|
||||
}
|
||||
|
||||
[HttpPost("local/ensure-storage")]
|
||||
public async Task<IActionResult> EnsureStorage(CancellationToken cancellationToken)
|
||||
private async Task<FileDownloadResult> DownloadReportPackageWithRetryAsync(
|
||||
GlobalLeaksStoredSession session,
|
||||
ReportDto report,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
await _inboxService.EnsureStorageReadyAsync(cancellationToken);
|
||||
return Ok(new { ok = true });
|
||||
}
|
||||
|
||||
[HttpPost("local/process")]
|
||||
public async Task<ActionResult<ImportSummary>> ProcessLocalZips(CancellationToken cancellationToken)
|
||||
=> Ok(await _inboxService.ProcessPendingFolderZipsAsync(cancellationToken));
|
||||
|
||||
[HttpGet("local/zips")]
|
||||
public async Task<ActionResult<IReadOnlyList<string>>> GetLocalZips(CancellationToken cancellationToken)
|
||||
=> Ok(await _inboxService.GetExistingZipNamesAsync(cancellationToken));
|
||||
|
||||
[HttpDelete("local/zips/{zipName}")]
|
||||
public async Task<IActionResult> DeleteLocalZip(string zipName, CancellationToken cancellationToken)
|
||||
for (var attempt = 0; ; attempt++)
|
||||
{
|
||||
await _inboxService.DeleteZipAsync(zipName, cancellationToken);
|
||||
return Ok(new { ok = true });
|
||||
try
|
||||
{
|
||||
return await _globalLeaksClient.DownloadReportPackageAsync(
|
||||
session.SessionId!,
|
||||
report.Id,
|
||||
cancellationToken,
|
||||
session.DpopPrivateKey);
|
||||
}
|
||||
catch (GlobalLeaksValidationException ex) when (IsExportNotReady(ex) && attempt < ExportRetryDelays.Length)
|
||||
{
|
||||
var delay = ExportRetryDelays[attempt];
|
||||
_logger.LogWarning(
|
||||
ex,
|
||||
"GlobalLeaks aun no ha preparado la exportacion de la denuncia {ReportId}. Reintento {Attempt}/{Total} en {DelaySeconds}s.",
|
||||
report.Id,
|
||||
attempt + 1,
|
||||
ExportRetryDelays.Length,
|
||||
delay.TotalSeconds);
|
||||
|
||||
await Task.Delay(delay, cancellationToken);
|
||||
}
|
||||
catch (GlobalLeaksValidationException ex) when (IsExportNotReady(ex))
|
||||
{
|
||||
throw new GlobalLeaksValidationException(
|
||||
"GlobalLeaks todavia esta preparando la exportacion de esta denuncia. Espera unos segundos y vuelve a importarla.",
|
||||
StatusCodes.Status503ServiceUnavailable);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static bool IsExportNotReady(GlobalLeaksValidationException ex)
|
||||
=> ex.StatusCode is >= 500 and <= 504;
|
||||
|
||||
private ActionResult ToGlobalLeaksApiError(GlobalLeaksValidationException ex, string operation)
|
||||
{
|
||||
if (ex.StatusCode == StatusCodes.Status401Unauthorized ||
|
||||
ex.StatusCode == StatusCodes.Status403Forbidden)
|
||||
{
|
||||
_logger.LogWarning(
|
||||
"GlobalLeaks rechazo la operacion '{Operation}'. Status={StatusCode}. Mensaje={Message}",
|
||||
operation,
|
||||
ex.StatusCode,
|
||||
ex.Message);
|
||||
|
||||
if (IsSessionAuthorizationProblem(ex.Message))
|
||||
{
|
||||
return new ObjectResult(new ApiError(
|
||||
"La sesion de GlobalLeaks no es valida o ha caducado. Renueva el 2FA y vuelve a intentarlo.",
|
||||
SessionExpired: true))
|
||||
{
|
||||
StatusCode = StatusCodes.Status401Unauthorized
|
||||
};
|
||||
}
|
||||
|
||||
if (IsDpopValidationProblem(ex.Message))
|
||||
{
|
||||
return new ObjectResult(new ApiError(
|
||||
"GlobalLeaks no ha podido validar la proteccion de la sesion (DPoP). La sesion no se ha marcado como caducada. Vuelve a intentarlo y, si se repite, avisa al equipo tecnico."))
|
||||
{
|
||||
StatusCode = StatusCodes.Status502BadGateway
|
||||
};
|
||||
}
|
||||
|
||||
if (IsMissingAuthenticationHeadersProblem(ex.Message))
|
||||
{
|
||||
return new ObjectResult(new ApiError(
|
||||
"No se ha podido completar la autenticacion de la llamada a GlobalLeaks. La sesion no se ha marcado como caducada. Vuelve a intentarlo y, si se repite, avisa al equipo tecnico."))
|
||||
{
|
||||
StatusCode = StatusCodes.Status502BadGateway
|
||||
};
|
||||
}
|
||||
|
||||
var permissionMessage = operation.Contains("bandeja", StringComparison.OrdinalIgnoreCase)
|
||||
? "GlobalLeaks no ha autorizado la carga de la bandeja para este usuario. La sesion sigue activa; comprueba los permisos del usuario en el buzon."
|
||||
: "No tienes permiso en GlobalLeaks para acceder a esa denuncia. Puede ser una denuncia anterior a la creacion de tu usuario; consulta con el administrador del buzon.";
|
||||
|
||||
return new ObjectResult(new ApiError(
|
||||
permissionMessage))
|
||||
{
|
||||
StatusCode = StatusCodes.Status403Forbidden
|
||||
};
|
||||
}
|
||||
|
||||
if (ex.StatusCode is >= 500 and <= 504)
|
||||
{
|
||||
return new ObjectResult(new ApiError(
|
||||
$"GlobalLeaks no ha podido {operation} en este momento. Intentalo de nuevo en unos segundos."))
|
||||
{
|
||||
StatusCode = StatusCodes.Status503ServiceUnavailable
|
||||
};
|
||||
}
|
||||
|
||||
return new ObjectResult(new ApiError(ex.Message))
|
||||
{
|
||||
StatusCode = ex.StatusCode
|
||||
};
|
||||
}
|
||||
|
||||
private static bool IsSessionAuthorizationProblem(string? message)
|
||||
=> !string.IsNullOrWhiteSpace(message) &&
|
||||
(message.Contains("NotAuthenticated", StringComparison.OrdinalIgnoreCase) ||
|
||||
message.Contains("Invalid token", StringComparison.OrdinalIgnoreCase) ||
|
||||
message.Contains("Invalid session", StringComparison.OrdinalIgnoreCase) ||
|
||||
message.Contains("session", StringComparison.OrdinalIgnoreCase) && message.Contains("expired", StringComparison.OrdinalIgnoreCase) ||
|
||||
message.Contains("token", StringComparison.OrdinalIgnoreCase) && message.Contains("expired", StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
private static bool IsDpopValidationProblem(string? message)
|
||||
=> !string.IsNullOrWhiteSpace(message) &&
|
||||
message.Contains("Invalid DPoP proof", StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
private static bool IsMissingAuthenticationHeadersProblem(string? message)
|
||||
=> !string.IsNullOrWhiteSpace(message) &&
|
||||
message.Contains("No token and no session", StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
private async Task<GlobalLeaksStoredSession?> RequireActiveSessionAsync(string username, CancellationToken cancellationToken)
|
||||
{
|
||||
@@ -322,4 +529,7 @@ public sealed class InboxController : ControllerBase
|
||||
=> session is null
|
||||
? null
|
||||
: new ApiGlobalLeaksSessionDto(session.Username, session.Role, session.HasActiveSession, session.UpdatedAt);
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ using Microsoft.AspNetCore.Mvc;
|
||||
namespace ApiDenuncias.Controllers;
|
||||
|
||||
[ApiController]
|
||||
[Authorize]
|
||||
[Authorize(Policy = "ConfigurationAdministrators")]
|
||||
[Route("api/purge")]
|
||||
public sealed class PurgeController : ControllerBase
|
||||
{
|
||||
|
||||
@@ -34,14 +34,38 @@ public sealed class TrackingController : ControllerBase
|
||||
return Ok(new { ok = true });
|
||||
}
|
||||
|
||||
[HttpPost("handled-in-gestiona")]
|
||||
public async Task<IActionResult> MarkHandledInGestiona(
|
||||
MarkReportHandledInGestionaRequest request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
await _trackingService.MarkReportHandledInGestionaAsync(
|
||||
GetUsername(),
|
||||
request.DenunciaId,
|
||||
request.UploadedAtUtc,
|
||||
cancellationToken);
|
||||
return Ok(new { ok = true });
|
||||
}
|
||||
|
||||
[HttpPost("import-permission")]
|
||||
public async Task<IActionResult> EnsureImportPermission(
|
||||
TrackingImportPermissionRequest request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
await _trackingService.EnsureReportCanBeImportedByUserAsync(GetUsername(), request.Report, cancellationToken);
|
||||
try
|
||||
{
|
||||
await _trackingService.EnsureReportCanBeImportedByUserAsync(
|
||||
GetUsername(),
|
||||
request.Report,
|
||||
request.ConfirmDifferentOwner,
|
||||
cancellationToken);
|
||||
return Ok(new { ok = true });
|
||||
}
|
||||
catch (ReportOwnershipException ex)
|
||||
{
|
||||
return Conflict(new ApiError(ex.Message));
|
||||
}
|
||||
}
|
||||
|
||||
private string GetUsername()
|
||||
=> User.Identity?.Name ?? throw new InvalidOperationException("No hay usuario autenticado.");
|
||||
|
||||
@@ -121,15 +121,26 @@ public static class GlobalLeaksJsonEnricher
|
||||
}
|
||||
}
|
||||
|
||||
if (element.TryGetProperty("children", out var children) &&
|
||||
children.ValueKind == JsonValueKind.Array)
|
||||
foreach (var property in element.EnumerateObject())
|
||||
{
|
||||
foreach (var child in children.EnumerateArray())
|
||||
if (property.NameEquals("options"))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (property.Value.ValueKind == JsonValueKind.Object)
|
||||
{
|
||||
CollectDefinitions(property.Value, definitions);
|
||||
}
|
||||
else if (property.Value.ValueKind == JsonValueKind.Array)
|
||||
{
|
||||
foreach (var child in property.Value.EnumerateArray())
|
||||
{
|
||||
CollectDefinitions(child, definitions);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static Dictionary<string, string> ParseOptions(JsonElement element)
|
||||
{
|
||||
@@ -163,19 +174,35 @@ public static class GlobalLeaksJsonEnricher
|
||||
|
||||
private static string ResolveAnswer(JsonElement answerArray, FieldDefinition definition)
|
||||
{
|
||||
if (answerArray.ValueKind == JsonValueKind.Object)
|
||||
{
|
||||
return ResolveAnswerObject(answerArray, definition);
|
||||
}
|
||||
|
||||
if (answerArray.ValueKind != JsonValueKind.Array)
|
||||
{
|
||||
return string.Empty;
|
||||
return ResolveValue(answerArray, definition.Options);
|
||||
}
|
||||
|
||||
var values = new List<string>();
|
||||
|
||||
foreach (var answer in answerArray.EnumerateArray())
|
||||
{
|
||||
if (answer.ValueKind != JsonValueKind.Object)
|
||||
var resolved = answer.ValueKind == JsonValueKind.Object
|
||||
? ResolveAnswerObject(answer, definition)
|
||||
: ResolveValue(answer, definition.Options);
|
||||
if (!string.IsNullOrWhiteSpace(resolved))
|
||||
{
|
||||
continue;
|
||||
values.Add(resolved);
|
||||
}
|
||||
}
|
||||
|
||||
return string.Join("; ", values.Distinct(StringComparer.OrdinalIgnoreCase));
|
||||
}
|
||||
|
||||
private static string ResolveAnswerObject(JsonElement answer, FieldDefinition definition)
|
||||
{
|
||||
var values = new List<string>();
|
||||
|
||||
if (answer.TryGetProperty("value", out var valueElement))
|
||||
{
|
||||
@@ -200,7 +227,6 @@ public static class GlobalLeaksJsonEnricher
|
||||
values.Add(label);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return string.Join("; ", values.Distinct(StringComparer.OrdinalIgnoreCase));
|
||||
}
|
||||
@@ -213,10 +239,95 @@ public static class GlobalLeaksJsonEnricher
|
||||
JsonValueKind.True => "Sí",
|
||||
JsonValueKind.False => "No",
|
||||
JsonValueKind.Number => valueElement.GetRawText(),
|
||||
JsonValueKind.Array => ResolveArrayValue(valueElement, options),
|
||||
JsonValueKind.Object => ResolveObjectValue(valueElement, options),
|
||||
_ => string.Empty,
|
||||
};
|
||||
}
|
||||
|
||||
private static string ResolveArrayValue(JsonElement valueElement, Dictionary<string, string> options)
|
||||
{
|
||||
var values = valueElement
|
||||
.EnumerateArray()
|
||||
.Select(item => ResolveValue(item, options))
|
||||
.Where(value => !string.IsNullOrWhiteSpace(value))
|
||||
.Distinct(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
return string.Join("; ", values);
|
||||
}
|
||||
|
||||
private static string ResolveObjectValue(JsonElement valueElement, Dictionary<string, string> options)
|
||||
{
|
||||
foreach (var propertyName in new[] { "label", "text", "value", "answer", "date", "formatted", "name" })
|
||||
{
|
||||
if (!valueElement.TryGetProperty(propertyName, out var propertyValue))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var resolved = ResolveValue(propertyValue, options);
|
||||
if (!string.IsNullOrWhiteSpace(resolved))
|
||||
{
|
||||
return resolved;
|
||||
}
|
||||
}
|
||||
|
||||
if (TryReadDateParts(valueElement, out var dateText))
|
||||
{
|
||||
return dateText;
|
||||
}
|
||||
|
||||
var values = new List<string>();
|
||||
foreach (var property in valueElement.EnumerateObject())
|
||||
{
|
||||
if (property.Value.ValueKind is JsonValueKind.True or JsonValueKind.False &&
|
||||
property.Value.GetBoolean() &&
|
||||
options.TryGetValue(property.Name, out var label))
|
||||
{
|
||||
values.Add(label);
|
||||
}
|
||||
}
|
||||
|
||||
return string.Join("; ", values.Distinct(StringComparer.OrdinalIgnoreCase));
|
||||
}
|
||||
|
||||
private static bool TryReadDateParts(JsonElement valueElement, out string dateText)
|
||||
{
|
||||
dateText = string.Empty;
|
||||
if (!TryGetIntProperty(valueElement, "day", out var day) ||
|
||||
!TryGetIntProperty(valueElement, "month", out var month) ||
|
||||
!TryGetIntProperty(valueElement, "year", out var year))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
dateText = new DateTime(year, month, day).ToString("dd/MM/yyyy", CultureInfo.InvariantCulture);
|
||||
return true;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private static bool TryGetIntProperty(JsonElement valueElement, string propertyName, out int value)
|
||||
{
|
||||
value = 0;
|
||||
if (!valueElement.TryGetProperty(propertyName, out var property))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return property.ValueKind switch
|
||||
{
|
||||
JsonValueKind.Number => property.TryGetInt32(out value),
|
||||
JsonValueKind.String => int.TryParse(property.GetString(), CultureInfo.InvariantCulture, out value),
|
||||
_ => false,
|
||||
};
|
||||
}
|
||||
|
||||
private static string ResolveStringValue(string? rawValue, Dictionary<string, string> options)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(rawValue))
|
||||
@@ -259,12 +370,13 @@ public static class GlobalLeaksJsonEnricher
|
||||
SetIfMissing(() => denuncia.OrganismoDenunciado, value => denuncia.OrganismoDenunciado = value, answers, "por favor indique el organismo o la institucion donde ha denunciado los hechos");
|
||||
SetIfMissing(() => denuncia.SolicitaProteccion, value => denuncia.SolicitaProteccion = value, answers, "solicita medidas concretas de proteccion");
|
||||
SetIfMissing(() => denuncia.MedidasProteccionSolicitadas, value => denuncia.MedidasProteccionSolicitadas = value, answers, "describa las medidas de proteccion solicitadas");
|
||||
SetIfMissing(() => denuncia.Lugar_Hechos, value => denuncia.Lugar_Hechos = value, answers, "lugar en la que ocurrieron los hechos que denuncia");
|
||||
SetIfMissing(() => denuncia.Lugar_Hechos, value => denuncia.Lugar_Hechos = value, answers, "lugar en el que ocurrieron los hechos que denuncia", "lugar en la que ocurrieron los hechos que denuncia", "lugar de los hechos");
|
||||
SetIfMissing(() => denuncia.AutorizaRemision, value => denuncia.AutorizaRemision = value, answers, "autorizacion para remitir su denuncia");
|
||||
SetIfMissing(() => denuncia.PreferenciaRemision, value => denuncia.PreferenciaRemision = value, answers, "en tal caso desea que su denuncia se remita anonimizada sin datos personales");
|
||||
SetIfMissing(() => denuncia.Notificacion_Preferencia, value => denuncia.Notificacion_Preferencia = value, answers, "seleccione su preferencia de notificacion y seguimiento de su denuncia", "notificaciones");
|
||||
SetIfMissing(() => denuncia.Notificacion_Preferencia, value => denuncia.Notificacion_Preferencia = value, answers, "preferencia de notificacion", "seleccione su preferencia de notificacion y seguimiento de su denuncia", "preferencia de notificacion y seguimiento de su denuncia", "notificaciones");
|
||||
SetIfMissing(() => denuncia.Notificacion_Electronica, value => denuncia.Notificacion_Electronica = value, answers, "notificaciones electronicas");
|
||||
SetIfMissing(() => denuncia.SeguimientoOnline, value => denuncia.SeguimientoOnline = value, answers, "seguimiento online");
|
||||
SetIfMissing(() => denuncia.SeguimientoOnline, value => denuncia.SeguimientoOnline = value, answers, "seguimiento online", "seguimiento de su denuncia");
|
||||
SetIfMissing(() => denuncia.Notificacion_Sms, value => denuncia.Notificacion_Sms = value, answers, "autorizo recibir notificaciones via sms", "autorizacion notificaciones via sms", "autoriza notificaciones via sms", "notificaciones via sms", "sms");
|
||||
SetIfMissing(() => denuncia.NotificacionPostal, value => denuncia.NotificacionPostal = value, answers, "autorizo recibir notificaciones via correo postal");
|
||||
SetIfMissing(() => denuncia.Correo_Electronico, value => denuncia.Correo_Electronico = value, answers, "correo electronico", "email");
|
||||
SetIfMissing(() => denuncia.Telefono, value => denuncia.Telefono = value, answers, "contacto telefonico", "telefono", "telefono movil");
|
||||
@@ -282,8 +394,8 @@ public static class GlobalLeaksJsonEnricher
|
||||
SetIfMissing(() => denuncia.Pais, value => denuncia.Pais = value, answers, "pais");
|
||||
|
||||
if (denuncia.Fecha_Hechos == DateTime.MinValue &&
|
||||
TryGetAnswer(answers, out var fechaHechos, "fecha de los hechos que denuncia") &&
|
||||
DateTime.TryParse(fechaHechos, CultureInfo.CurrentCulture, DateTimeStyles.None, out var parsedDate))
|
||||
TryGetAnswer(answers, out var fechaHechos, "fecha de los hechos que denuncia", "fecha de los hechos") &&
|
||||
TryParseReportDate(fechaHechos, out var parsedDate))
|
||||
{
|
||||
denuncia.Fecha_Hechos = parsedDate;
|
||||
}
|
||||
@@ -371,6 +483,38 @@ public static class GlobalLeaksJsonEnricher
|
||||
return false;
|
||||
}
|
||||
|
||||
private static bool TryParseReportDate(string? value, out DateTime parsedDate)
|
||||
{
|
||||
parsedDate = DateTime.MinValue;
|
||||
if (string.IsNullOrWhiteSpace(value))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
foreach (var culture in new[]
|
||||
{
|
||||
CultureInfo.GetCultureInfo("es-ES"),
|
||||
CultureInfo.InvariantCulture,
|
||||
CultureInfo.CurrentCulture
|
||||
})
|
||||
{
|
||||
if (DateTime.TryParse(value, culture, DateTimeStyles.None, out parsedDate))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var format in new[] { "dd/MM/yyyy", "d/M/yyyy", "yyyy-MM-dd" })
|
||||
{
|
||||
if (DateTime.TryParseExact(value, format, CultureInfo.InvariantCulture, DateTimeStyles.None, out parsedDate))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static string Normalize(string? text)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(text))
|
||||
|
||||
@@ -32,6 +32,7 @@ builder.Services.AddSingleton<LoginRateLimiter>();
|
||||
builder.Services.AddSingleton<GlobalLeaksSessionStore>();
|
||||
builder.Services.AddSingleton<PendingGlobalLeaksLoginStore>();
|
||||
builder.Services.AddScoped<GlobalLeaksClient>();
|
||||
builder.Services.AddScoped<GlobalLeaksSessionKeepAliveService>();
|
||||
builder.Services.AddSingleton<MySqlConnectionStringProvider>();
|
||||
builder.Services.AddScoped<MySqlDenunciaStore>();
|
||||
builder.Services.AddSingleton<IEncryptionKeyProvider, KeyVaultEncryptionKeyProvider>();
|
||||
@@ -42,7 +43,9 @@ builder.Services.AddScoped<IFilteredDenunciaStore>(sp => sp.GetRequiredService<E
|
||||
builder.Services.AddScoped<IInboxTrackingService, InboxTrackingService>();
|
||||
builder.Services.AddScoped<DenunciaInboxService>();
|
||||
builder.Services.AddScoped<GestionaDocumentWorkflowService>();
|
||||
builder.Services.AddScoped<GestionaExpedienteExceptionStore>();
|
||||
builder.Services.AddScoped<UserComplaintAccessService>();
|
||||
builder.Services.AddScoped<WorkGroupAdministrationService>();
|
||||
builder.Services.AddHttpClient<ManualPurgeService>();
|
||||
builder.Services.AddScoped<AppConfigurationService>();
|
||||
|
||||
@@ -99,7 +102,26 @@ builder.Services
|
||||
};
|
||||
});
|
||||
|
||||
builder.Services.AddAuthorization();
|
||||
var configurationAdministrators = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
"pcornejo",
|
||||
"Rgarciaglbk",
|
||||
"eaguilarGestor"
|
||||
};
|
||||
|
||||
builder.Services.AddAuthorization(options =>
|
||||
{
|
||||
options.AddPolicy("ConfigurationAdministrators", policy =>
|
||||
{
|
||||
policy.RequireAssertion(context =>
|
||||
{
|
||||
var username = context.User.Identity?.Name?.Trim();
|
||||
return context.User.Identity?.IsAuthenticated == true &&
|
||||
!string.IsNullOrWhiteSpace(username) &&
|
||||
configurationAdministrators.Contains(username);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
var app = builder.Build();
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -64,6 +64,9 @@ CREATE TABLE IF NOT EXISTS complaints (
|
||||
workflow_status TEXT NOT NULL,
|
||||
selected_document_name TEXT NULL,
|
||||
gestiona_uploaded_at_utc DATETIME(6) NULL,
|
||||
gestiona_last_upload_type TEXT NOT NULL,
|
||||
gestiona_assigned_group TEXT NOT NULL,
|
||||
pending_update_source VARCHAR(256) NOT NULL DEFAULT '',
|
||||
is_in_gestiona TINYINT(1) NOT NULL DEFAULT 0,
|
||||
is_rejected TINYINT(1) NOT NULL DEFAULT 0,
|
||||
created_at_utc DATETIME(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6),
|
||||
@@ -85,6 +88,58 @@ CREATE TABLE IF NOT EXISTS app_users (
|
||||
UNIQUE KEY uq_app_users_username (username)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS work_groups (
|
||||
id BIGINT NOT NULL AUTO_INCREMENT,
|
||||
code VARCHAR(32) NOT NULL,
|
||||
name VARCHAR(256) NOT NULL,
|
||||
is_active TINYINT(1) NOT NULL DEFAULT 1,
|
||||
created_at_utc DATETIME(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6),
|
||||
updated_at_utc DATETIME(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6) ON UPDATE CURRENT_TIMESTAMP(6),
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE KEY uq_work_groups_code (code)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
|
||||
|
||||
INSERT INTO work_groups (code, name)
|
||||
VALUES
|
||||
('600', 'Asuntos Juridicos y Proteccion a la Persona Denunciante'),
|
||||
('510', 'SDI - Investigacion Entradas')
|
||||
ON DUPLICATE KEY UPDATE
|
||||
name = VALUES(name),
|
||||
is_active = 1;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS app_user_groups (
|
||||
app_user_id BIGINT NOT NULL,
|
||||
work_group_id BIGINT NOT NULL,
|
||||
assigned_at_utc DATETIME(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6),
|
||||
assigned_by_username VARCHAR(256) NOT NULL DEFAULT '',
|
||||
PRIMARY KEY (app_user_id, work_group_id),
|
||||
KEY ix_app_user_groups_group (work_group_id),
|
||||
CONSTRAINT fk_app_user_groups_user
|
||||
FOREIGN KEY (app_user_id) REFERENCES app_users(id)
|
||||
ON DELETE CASCADE,
|
||||
CONSTRAINT fk_app_user_groups_group
|
||||
FOREIGN KEY (work_group_id) REFERENCES work_groups(id)
|
||||
ON DELETE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS app_user_group_history (
|
||||
id BIGINT NOT NULL AUTO_INCREMENT,
|
||||
app_user_id BIGINT NOT NULL,
|
||||
work_group_id BIGINT NOT NULL,
|
||||
action VARCHAR(16) NOT NULL,
|
||||
changed_by_username VARCHAR(256) NOT NULL,
|
||||
changed_at_utc DATETIME(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6),
|
||||
PRIMARY KEY (id),
|
||||
KEY ix_app_user_group_history_user (app_user_id),
|
||||
KEY ix_app_user_group_history_group (work_group_id),
|
||||
CONSTRAINT fk_app_user_group_history_user
|
||||
FOREIGN KEY (app_user_id) REFERENCES app_users(id)
|
||||
ON DELETE CASCADE,
|
||||
CONSTRAINT fk_app_user_group_history_group
|
||||
FOREIGN KEY (work_group_id) REFERENCES work_groups(id)
|
||||
ON DELETE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS inbox_reports (
|
||||
id BIGINT NOT NULL AUTO_INCREMENT,
|
||||
global_report_uuid CHAR(36) NOT NULL,
|
||||
@@ -102,6 +157,7 @@ CREATE TABLE IF NOT EXISTS inbox_reports (
|
||||
last_seen_at_utc DATETIME(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6) ON UPDATE CURRENT_TIMESTAMP(6),
|
||||
last_downloaded_at_utc DATETIME(6) NULL,
|
||||
last_downloaded_by_user_id BIGINT NULL,
|
||||
owner_user_id BIGINT NULL,
|
||||
imported_complaint_report_id INT NULL,
|
||||
imported_to_store_at_utc DATETIME(6) NULL,
|
||||
created_at_utc DATETIME(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6),
|
||||
@@ -110,8 +166,12 @@ CREATE TABLE IF NOT EXISTS inbox_reports (
|
||||
UNIQUE KEY uq_inbox_reports_uuid (global_report_uuid),
|
||||
KEY ix_inbox_reports_progressive (progressive_id),
|
||||
KEY ix_inbox_reports_downloaded (last_downloaded_at_utc),
|
||||
KEY ix_inbox_reports_owner (owner_user_id),
|
||||
CONSTRAINT fk_inbox_reports_last_user
|
||||
FOREIGN KEY (last_downloaded_by_user_id) REFERENCES app_users(id)
|
||||
ON DELETE SET NULL,
|
||||
CONSTRAINT fk_inbox_reports_owner_user
|
||||
FOREIGN KEY (owner_user_id) REFERENCES app_users(id)
|
||||
ON DELETE SET NULL
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
|
||||
|
||||
@@ -133,6 +193,24 @@ CREATE TABLE IF NOT EXISTS user_inbox_reports (
|
||||
ON DELETE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS gestiona_upload_history (
|
||||
id BIGINT NOT NULL AUTO_INCREMENT,
|
||||
external_report_id INT NOT NULL,
|
||||
gestiona_file_url TEXT NOT NULL,
|
||||
gestiona_file_code VARCHAR(128) NOT NULL DEFAULT '',
|
||||
upload_type VARCHAR(64) NOT NULL DEFAULT '',
|
||||
assigned_group_code VARCHAR(32) NOT NULL DEFAULT '',
|
||||
uploaded_by_username VARCHAR(256) NOT NULL DEFAULT '',
|
||||
uploaded_at_utc DATETIME(6) NOT NULL,
|
||||
subject TEXT NULL,
|
||||
document_names TEXT NULL,
|
||||
created_at_utc DATETIME(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6),
|
||||
PRIMARY KEY (id),
|
||||
KEY ix_gestiona_upload_history_report (external_report_id),
|
||||
KEY ix_gestiona_upload_history_uploaded_at (uploaded_at_utc),
|
||||
KEY ix_gestiona_upload_history_user (uploaded_by_username)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS complaint_attachments (
|
||||
id BIGINT NOT NULL AUTO_INCREMENT,
|
||||
complaint_id BIGINT NOT NULL,
|
||||
|
||||
@@ -35,7 +35,12 @@ public sealed class AppConfigurationService
|
||||
? null
|
||||
: Convert.ToString(value, CultureInfo.InvariantCulture);
|
||||
|
||||
return new AppConfigurationDto(string.IsNullOrWhiteSpace(dateText) ? null : dateText);
|
||||
var latestEncryptionKey = await GetLatestEncryptionKeyAsync(connection, cancellationToken);
|
||||
|
||||
return new AppConfigurationDto(
|
||||
string.IsNullOrWhiteSpace(dateText) ? null : dateText,
|
||||
latestEncryptionKey?.KeyDate.ToString("yyyy-MM-dd", CultureInfo.InvariantCulture),
|
||||
latestEncryptionKey?.Status);
|
||||
}
|
||||
|
||||
public async Task<AppConfigurationDto> SetExternalUpdateCutoffDateAsync(
|
||||
@@ -59,7 +64,42 @@ public sealed class AppConfigurationService
|
||||
command.Parameters.AddWithValue("@settingValue", string.IsNullOrWhiteSpace(dateText) ? DBNull.Value : dateText);
|
||||
|
||||
await command.ExecuteNonQueryAsync(cancellationToken);
|
||||
return new AppConfigurationDto(dateText);
|
||||
var latestEncryptionKey = await GetLatestEncryptionKeyAsync(connection, cancellationToken);
|
||||
return new AppConfigurationDto(
|
||||
dateText,
|
||||
latestEncryptionKey?.KeyDate.ToString("yyyy-MM-dd", CultureInfo.InvariantCulture),
|
||||
latestEncryptionKey?.Status);
|
||||
}
|
||||
|
||||
private static async Task<LatestEncryptionKey?> GetLatestEncryptionKeyAsync(
|
||||
MySqlConnection connection,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
try
|
||||
{
|
||||
await using var command = new MySqlCommand(
|
||||
"""
|
||||
SELECT key_date, status
|
||||
FROM encryption_keys
|
||||
ORDER BY key_date DESC
|
||||
LIMIT 1;
|
||||
""",
|
||||
connection);
|
||||
|
||||
await using var reader = await command.ExecuteReaderAsync(cancellationToken);
|
||||
if (!await reader.ReadAsync(cancellationToken))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return new LatestEncryptionKey(
|
||||
DateOnly.FromDateTime(reader.GetDateTime("key_date")),
|
||||
reader.GetString("status"));
|
||||
}
|
||||
catch (MySqlException ex) when (ex.Number == 1146)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<MySqlConnection> OpenConnectionAsync(CancellationToken cancellationToken)
|
||||
@@ -84,4 +124,6 @@ public sealed class AppConfigurationService
|
||||
connection);
|
||||
await command.ExecuteNonQueryAsync(cancellationToken);
|
||||
}
|
||||
|
||||
private sealed record LatestEncryptionKey(DateOnly KeyDate, string Status);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
using System.IO.Compression;
|
||||
using System.Globalization;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using ApiDenuncias.Helpers;
|
||||
using GestionaDenuncias.Shared.Models;
|
||||
@@ -7,8 +9,6 @@ namespace ApiDenuncias.Services;
|
||||
|
||||
public sealed class DenunciaInboxService
|
||||
{
|
||||
private const string RootPath = @"C:\ZipsDenuncias";
|
||||
|
||||
private static readonly HashSet<string> BlockedAttachmentExtensions = new(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
".ade",
|
||||
@@ -55,91 +55,47 @@ public sealed class DenunciaInboxService
|
||||
};
|
||||
|
||||
private readonly IGestionaService _gestionaService;
|
||||
private readonly GestionaExpedienteExceptionStore _gestionaExceptionStore;
|
||||
private readonly IDenunciaStore _denunciaStore;
|
||||
private readonly IFilteredDenunciaStore _filteredDenunciaStore;
|
||||
private readonly ILogger<DenunciaInboxService> _logger;
|
||||
|
||||
public DenunciaInboxService(
|
||||
IGestionaService gestionaService,
|
||||
GestionaExpedienteExceptionStore gestionaExceptionStore,
|
||||
IDenunciaStore denunciaStore,
|
||||
IFilteredDenunciaStore filteredDenunciaStore,
|
||||
ILogger<DenunciaInboxService> logger)
|
||||
{
|
||||
_gestionaService = gestionaService;
|
||||
_gestionaExceptionStore = gestionaExceptionStore;
|
||||
_denunciaStore = denunciaStore;
|
||||
_filteredDenunciaStore = filteredDenunciaStore;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public async Task EnsureStorageReadyAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
Directory.CreateDirectory(RootPath);
|
||||
await _denunciaStore.EnsureSchemaAsync(cancellationToken);
|
||||
}
|
||||
|
||||
public async Task<IReadOnlyList<string>> GetExistingZipNamesAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
await EnsureStorageReadyAsync(cancellationToken);
|
||||
return Directory
|
||||
.GetFiles(RootPath, "*.zip")
|
||||
.Select(Path.GetFileName)
|
||||
.Where(name => !string.IsNullOrWhiteSpace(name))
|
||||
.OrderBy(name => name, StringComparer.OrdinalIgnoreCase)
|
||||
.ToArray()!;
|
||||
}
|
||||
|
||||
public async Task DeleteZipAsync(string zipName, CancellationToken cancellationToken = default)
|
||||
{
|
||||
await EnsureStorageReadyAsync(cancellationToken);
|
||||
|
||||
var fullPath = Path.Combine(RootPath, zipName);
|
||||
if (File.Exists(fullPath))
|
||||
{
|
||||
File.Delete(fullPath);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<ImportSummary> ProcessPendingFolderZipsAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
await EnsureStorageReadyAsync(cancellationToken);
|
||||
|
||||
var zipPaths = Directory.GetFiles(RootPath, "*.zip")
|
||||
.OrderBy(path => path, StringComparer.OrdinalIgnoreCase)
|
||||
.ToList();
|
||||
|
||||
var errors = new List<string>();
|
||||
var warnings = new List<string>();
|
||||
var importedCount = 0;
|
||||
var complaintIds = new List<int>();
|
||||
|
||||
foreach (var zipPath in zipPaths)
|
||||
{
|
||||
try
|
||||
{
|
||||
var zipBytes = await File.ReadAllBytesAsync(zipPath, cancellationToken);
|
||||
var result = await ProcessZipAsync(zipBytes, Path.GetFileName(zipPath), null, cancellationToken);
|
||||
File.Delete(zipPath);
|
||||
importedCount++;
|
||||
complaintIds.Add(result.ComplaintId);
|
||||
warnings.AddRange(result.Warnings.Select(warning => $"{Path.GetFileName(zipPath)}: {warning}"));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error procesando el ZIP local {ZipPath}", zipPath);
|
||||
errors.Add($"{Path.GetFileName(zipPath)}: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
return new ImportSummary(zipPaths.Count, importedCount, errors, complaintIds, warnings);
|
||||
await _gestionaExceptionStore.EnsureSchemaAsync(cancellationToken);
|
||||
}
|
||||
|
||||
public async Task<ImportSummary> ImportFromGlobalLeaksAsync(
|
||||
FileDownloadResult zipDownload,
|
||||
FileDownloadResult reportDownload,
|
||||
FileDownloadResult? jsonDownload,
|
||||
ReportDetailDto? reportDetail,
|
||||
ReportDto inboxReport,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
await EnsureStorageReadyAsync(cancellationToken);
|
||||
|
||||
var fileName = string.IsNullOrWhiteSpace(zipDownload.FileName)
|
||||
? $"report-{Guid.NewGuid():N}.zip"
|
||||
: zipDownload.FileName;
|
||||
var sourceName = string.IsNullOrWhiteSpace(reportDownload.FileName)
|
||||
? $"report-{Guid.NewGuid():N}"
|
||||
: reportDownload.FileName;
|
||||
if (sourceName.EndsWith(".zip", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
sourceName = Path.GetFileNameWithoutExtension(sourceName);
|
||||
}
|
||||
|
||||
var json = jsonDownload is null
|
||||
? null
|
||||
@@ -147,24 +103,79 @@ public sealed class DenunciaInboxService
|
||||
|
||||
try
|
||||
{
|
||||
var result = await ProcessZipAsync(zipDownload.Content, fileName, json, cancellationToken);
|
||||
return new ImportSummary(1, 1, [], [result.ComplaintId], result.Warnings);
|
||||
var result = await ProcessGlobalLeaksPackageAsync(
|
||||
reportDownload.Content,
|
||||
sourceName,
|
||||
json,
|
||||
reportDetail,
|
||||
inboxReport,
|
||||
cancellationToken);
|
||||
return new ImportSummary(
|
||||
1,
|
||||
result.ImportedCount,
|
||||
[],
|
||||
result.ImportedCount > 0 ? [result.ComplaintId] : [],
|
||||
result.Warnings);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error importando denuncia desde GlobalLeaks {FileName}", fileName);
|
||||
return new ImportSummary(1, 0, [$"{fileName}: {ex.Message}"]);
|
||||
_logger.LogError(ex, "Error importando denuncia desde GlobalLeaks {SourceName}", sourceName);
|
||||
return new ImportSummary(1, 0, [ToUserImportErrorMessage(sourceName, ex)]);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<ProcessZipResult> ProcessZipAsync(
|
||||
byte[] zipBytes,
|
||||
private static string ToUserImportErrorMessage(string sourceName, Exception ex)
|
||||
{
|
||||
var message = ex.Message ?? string.Empty;
|
||||
|
||||
if (ex is EncryptionKeyUnavailableException ||
|
||||
message.Contains("encryption_keys", StringComparison.OrdinalIgnoreCase) ||
|
||||
message.Contains("clave de cifrado", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return $"{sourceName}: no hay una clave de cifrado activa para guardar la denuncia. Avisa al equipo tecnico.";
|
||||
}
|
||||
|
||||
if (message.Contains("max_allowed_packet", StringComparison.OrdinalIgnoreCase) ||
|
||||
message.Contains("Error submitting", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return $"{sourceName}: la denuncia contiene un adjunto demasiado grande para guardarse. Revisa los adjuntos o consulta con soporte.";
|
||||
}
|
||||
|
||||
if (message.Contains("Connection must be Open", StringComparison.OrdinalIgnoreCase) ||
|
||||
message.Contains("current state is Closed", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return $"{sourceName}: se ha perdido la conexion mientras se guardaba la denuncia. Vuelve a intentarlo.";
|
||||
}
|
||||
|
||||
if (ex is InvalidDataException)
|
||||
{
|
||||
return $"{sourceName}: el paquete descargado de GlobalLeaks no se ha podido leer correctamente. Vuelve a intentarlo.";
|
||||
}
|
||||
|
||||
if (ex is InvalidOperationException && IsUserFacingImportMessage(message))
|
||||
{
|
||||
return $"{sourceName}: {message}";
|
||||
}
|
||||
|
||||
return $"{sourceName}: no se ha podido importar la denuncia. Vuelve a intentarlo; si se repite, avisa al equipo tecnico.";
|
||||
}
|
||||
|
||||
private static bool IsUserFacingImportMessage(string message)
|
||||
=> message.StartsWith("El paquete de GlobalLeaks", StringComparison.OrdinalIgnoreCase) ||
|
||||
message.StartsWith("El report viene", StringComparison.OrdinalIgnoreCase) ||
|
||||
message.StartsWith("El archivo", StringComparison.OrdinalIgnoreCase) ||
|
||||
message.StartsWith("Denuncia no disponible", StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
private async Task<ProcessPackageResult> ProcessGlobalLeaksPackageAsync(
|
||||
byte[] packageBytes,
|
||||
string sourceName,
|
||||
string? globalLeaksJson,
|
||||
ReportDetailDto? reportDetail,
|
||||
ReportDto inboxReport,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
using var zipStream = new MemoryStream(zipBytes, writable: false);
|
||||
using var archive = new ZipArchive(zipStream, ZipArchiveMode.Read, leaveOpen: false);
|
||||
using var packageStream = new MemoryStream(packageBytes, writable: false);
|
||||
using var archive = new ZipArchive(packageStream, ZipArchiveMode.Read, leaveOpen: false);
|
||||
|
||||
var reportEntry = FindReportEntry(archive);
|
||||
|
||||
@@ -178,8 +189,8 @@ public sealed class DenunciaInboxService
|
||||
|
||||
throw new InvalidOperationException(
|
||||
entries.Length == 0
|
||||
? "El ZIP no contiene ficheros."
|
||||
: $"El ZIP no contiene un report reconocible. Ficheros encontrados: {string.Join(", ", entries)}");
|
||||
? "El paquete de GlobalLeaks no contiene ficheros."
|
||||
: $"El paquete de GlobalLeaks no contiene un report reconocible. Ficheros encontrados: {string.Join(", ", entries)}");
|
||||
}
|
||||
|
||||
var reportIsPdf = IsPdfEntry(reportEntry);
|
||||
@@ -211,6 +222,12 @@ public sealed class DenunciaInboxService
|
||||
$"No se ha podido determinar el identificador de la denuncia en {sourceName}.");
|
||||
}
|
||||
|
||||
denuncia.PendingUpdateSource = inboxReport.CitizenHasNewActivity
|
||||
? ComplaintUpdateSources.Citizen
|
||||
: inboxReport.ReceiverHasNewActivity
|
||||
? ComplaintUpdateSources.Receiver
|
||||
: string.Empty;
|
||||
|
||||
if (reportIsPdf)
|
||||
{
|
||||
reportText = BuildSyntheticReportText(denuncia);
|
||||
@@ -224,16 +241,54 @@ public sealed class DenunciaInboxService
|
||||
denuncia.Expediente_Gestiona = "Pendiente";
|
||||
}
|
||||
|
||||
var readFilesResult = await ReadFilesFromArchiveAsync(archive, reportEntry, denuncia.Id_Denuncia, cancellationToken);
|
||||
var readFilesResult = await ReadFilesFromArchiveAsync(
|
||||
archive,
|
||||
reportEntry,
|
||||
denuncia.Id_Denuncia,
|
||||
denuncia.Fecha,
|
||||
reportDetail,
|
||||
cancellationToken);
|
||||
await MergeComplaintAsync(denuncia, cancellationToken);
|
||||
var storedComplaintBeforeFiles = await _denunciaStore.GetDenunciaByIdAsync(denuncia.Id_Denuncia, cancellationToken);
|
||||
var existingFiles = IsAlreadyUploadedToGestiona(storedComplaintBeforeFiles)
|
||||
? await GetFicherosMetadataByDenunciaForImportAsync(denuncia.Id_Denuncia, cancellationToken)
|
||||
: [];
|
||||
PreserveKnownUploadedReportStatus(readFilesResult.Files, existingFiles, storedComplaintBeforeFiles);
|
||||
await MergeFilesAsync(readFilesResult.Files, cancellationToken);
|
||||
return new ProcessZipResult(denuncia.Id_Denuncia, readFilesResult.Warnings);
|
||||
|
||||
var warnings = readFilesResult.Warnings.ToList();
|
||||
var storedComplaint = await _denunciaStore.GetDenunciaByIdAsync(denuncia.Id_Denuncia, cancellationToken);
|
||||
if (IsAlreadyUploadedToGestiona(storedComplaint))
|
||||
{
|
||||
var storedFiles = await GetFicherosMetadataByDenunciaForImportAsync(
|
||||
denuncia.Id_Denuncia,
|
||||
cancellationToken);
|
||||
if (!HasPendingFilesForGestiona(storedFiles))
|
||||
{
|
||||
storedComplaint!.EsActualizacion = false;
|
||||
storedComplaint.EnGestiona = true;
|
||||
storedComplaint.PendingUpdateSource = string.Empty;
|
||||
await _denunciaStore.UpsertDenunciaAsync(storedComplaint, cancellationToken);
|
||||
|
||||
warnings.Add(BuildNoCitizenUpdateWarning(inboxReport));
|
||||
return new ProcessPackageResult(denuncia.Id_Denuncia, 0, warnings);
|
||||
}
|
||||
}
|
||||
|
||||
return new ProcessPackageResult(denuncia.Id_Denuncia, 1, warnings);
|
||||
}
|
||||
|
||||
private Task<List<FicherosDenuncias>> GetFicherosMetadataByDenunciaForImportAsync(
|
||||
int denunciaId,
|
||||
CancellationToken cancellationToken)
|
||||
=> _filteredDenunciaStore.GetFicherosMetadataByDenunciaAsync(denunciaId, cancellationToken);
|
||||
|
||||
private async Task<ReadFilesResult> ReadFilesFromArchiveAsync(
|
||||
ZipArchive archive,
|
||||
ZipArchiveEntry reportEntry,
|
||||
int denunciaId,
|
||||
DateTime reportDateUtc,
|
||||
ReportDetailDto? reportDetail,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var warnings = new List<string>();
|
||||
@@ -243,9 +298,9 @@ public sealed class DenunciaInboxService
|
||||
id_Fichero: 0,
|
||||
id_Tipo: 1,
|
||||
descripcion: IsPdfEntry(reportEntry) ? "report.pdf original" : "report.txt original",
|
||||
fecha: reportEntry.LastWriteTime.UtcDateTime == DateTime.MinValue
|
||||
? DateTime.UtcNow
|
||||
: reportEntry.LastWriteTime.UtcDateTime,
|
||||
fecha: reportDateUtc == DateTime.MinValue
|
||||
? GetEntryFallbackDateUtc(reportEntry)
|
||||
: NormalizeUtc(reportDateUtc),
|
||||
observaciones: "",
|
||||
id_Denuncia: denunciaId,
|
||||
nombreFichero: IsPdfEntry(reportEntry) ? "report.pdf" : "report.txt",
|
||||
@@ -264,9 +319,7 @@ public sealed class DenunciaInboxService
|
||||
id_Fichero: 0,
|
||||
id_Tipo: 1,
|
||||
descripcion: null,
|
||||
fecha: entry.LastWriteTime.UtcDateTime == DateTime.MinValue
|
||||
? DateTime.UtcNow
|
||||
: entry.LastWriteTime.UtcDateTime,
|
||||
fecha: ResolveAttachmentDateUtc(entry, reportDetail),
|
||||
observaciones: "",
|
||||
id_Denuncia: denunciaId,
|
||||
nombreFichero: Path.GetFileName(entry.FullName),
|
||||
@@ -308,6 +361,74 @@ public sealed class DenunciaInboxService
|
||||
return _denunciaStore.UpsertFicherosAsync(nuevosFicheros, cancellationToken);
|
||||
}
|
||||
|
||||
private static void PreserveKnownUploadedReportStatus(
|
||||
List<FicherosDenuncias> newFiles,
|
||||
IReadOnlyList<FicherosDenuncias> existingFiles,
|
||||
DenunciasGestiona? storedComplaint)
|
||||
{
|
||||
if (!IsAlreadyUploadedToGestiona(storedComplaint) || existingFiles.Count == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var existingReportHashes = existingFiles
|
||||
.Where(file => file.EsReport && !string.IsNullOrWhiteSpace(file.ContentSha256))
|
||||
.GroupBy(file => file.ContentSha256, StringComparer.OrdinalIgnoreCase)
|
||||
.ToDictionary(group => group.Key, group => group.First(), StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
foreach (var report in newFiles.Where(file => file.EsReport))
|
||||
{
|
||||
var hash = string.IsNullOrWhiteSpace(report.ContentSha256)
|
||||
? ComputeSha256Hex(report.Fichero ?? [])
|
||||
: report.ContentSha256.Trim().ToLowerInvariant();
|
||||
report.ContentSha256 = hash;
|
||||
|
||||
if (!existingReportHashes.TryGetValue(hash, out var existingReport))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
report.Subido = true;
|
||||
report.FechaSubida = existingReport.FechaSubida ??
|
||||
(storedComplaint!.FechaSubidaAGestiona == DateTime.MinValue
|
||||
? null
|
||||
: storedComplaint.FechaSubidaAGestiona);
|
||||
}
|
||||
}
|
||||
|
||||
private static string BuildNoCitizenUpdateWarning(ReportDto inboxReport)
|
||||
{
|
||||
return inboxReport.ReceiverHasNewActivity && !inboxReport.CitizenHasNewActivity
|
||||
? "La actividad detectada procede de la OAAF, pero el report descargado no contiene cambios pendientes de subir a Gestiona."
|
||||
: "La denuncia ya esta en Gestiona y el report no contiene documentos ni comentarios nuevos del ciudadano pendientes de subir.";
|
||||
}
|
||||
|
||||
private static bool HasPendingFilesForGestiona(IReadOnlyList<FicherosDenuncias> files)
|
||||
{
|
||||
var plannedHashes = files
|
||||
.Where(file => file.Subido && !string.IsNullOrWhiteSpace(file.ContentSha256))
|
||||
.Select(file => file.ContentSha256)
|
||||
.ToHashSet(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
foreach (var file in files.Where(file => !file.Subido))
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(file.ContentSha256))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (plannedHashes.Add(file.ContentSha256))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static string ComputeSha256Hex(byte[] content)
|
||||
=> Convert.ToHexString(SHA256.HashData(content)).ToLowerInvariant();
|
||||
|
||||
private async Task CompleteGestionaStatusAsync(
|
||||
DenunciasGestiona denuncia,
|
||||
CancellationToken cancellationToken)
|
||||
@@ -329,16 +450,16 @@ public sealed class DenunciaInboxService
|
||||
{
|
||||
var match = await _gestionaService.BuscarExpedientePorIdEnAsuntoAsync(denuncia.Id_Denuncia);
|
||||
if (match is null)
|
||||
{
|
||||
match = await TryFindGestionaExceptionMatchAsync(denuncia, cancellationToken);
|
||||
if (match is null)
|
||||
{
|
||||
ApplyPendingGestionaStatus(denuncia);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
denuncia.EsActualizacion = true;
|
||||
denuncia.EnGestiona = true;
|
||||
denuncia.Expediente_Gestiona = match.FileUrl;
|
||||
denuncia.CodigoExpedienteGestiona = match.CodigoExpediente ?? string.Empty;
|
||||
denuncia.NombreDenuncia = match.FreeTitle ?? $"Denuncia {denuncia.Id_Denuncia}-CD";
|
||||
ApplyGestionaMatch(denuncia, match);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
@@ -350,6 +471,39 @@ public sealed class DenunciaInboxService
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<GestionaExpedienteInfo?> TryFindGestionaExceptionMatchAsync(
|
||||
DenunciasGestiona denuncia,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var exception = await _gestionaExceptionStore.FindAsync(denuncia, cancellationToken);
|
||||
if (exception is null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var match = await _gestionaService.BuscarExpedientePorCodigoAsync(exception.GestionaFileCode);
|
||||
if (match is null)
|
||||
{
|
||||
_logger.LogWarning(
|
||||
"La denuncia {DenunciaId} tiene excepcion Gestiona con codigo {GestionaFileCode}, pero no se ha encontrado expediente en Gestiona.",
|
||||
denuncia.Id_Denuncia,
|
||||
exception.GestionaFileCode);
|
||||
}
|
||||
|
||||
return match;
|
||||
}
|
||||
|
||||
private static void ApplyGestionaMatch(
|
||||
DenunciasGestiona denuncia,
|
||||
GestionaExpedienteInfo match)
|
||||
{
|
||||
denuncia.EsActualizacion = true;
|
||||
denuncia.EnGestiona = true;
|
||||
denuncia.Expediente_Gestiona = match.FileUrl;
|
||||
denuncia.CodigoExpedienteGestiona = match.CodigoExpediente ?? string.Empty;
|
||||
denuncia.NombreDenuncia = match.FreeTitle ?? $"Denuncia {denuncia.Id_Denuncia}-CD";
|
||||
}
|
||||
|
||||
private static bool TryApplyGestionaStatusFromStore(
|
||||
DenunciasGestiona target,
|
||||
DenunciasGestiona? storedComplaint)
|
||||
@@ -461,6 +615,7 @@ public sealed class DenunciaInboxService
|
||||
target.Pais = source.Pais;
|
||||
target.CamposFormularioJson = source.CamposFormularioJson;
|
||||
target.TextoOriginalReport = source.TextoOriginalReport;
|
||||
target.PendingUpdateSource = source.PendingUpdateSource;
|
||||
}
|
||||
|
||||
private static bool IsSupportedAttachmentEntry(ZipArchiveEntry entry)
|
||||
@@ -474,6 +629,83 @@ public sealed class DenunciaInboxService
|
||||
return IsDirectChildOf(normalized, "files") || IsDirectChildOf(normalized, "files_attached_from_recipients");
|
||||
}
|
||||
|
||||
private static DateTime ResolveAttachmentDateUtc(ZipArchiveEntry entry, ReportDetailDto? reportDetail)
|
||||
{
|
||||
if (reportDetail is not null)
|
||||
{
|
||||
var normalized = NormalizeEntryPath(entry.FullName);
|
||||
var preferredFiles = IsDirectChildOf(normalized, "files_attached_from_recipients")
|
||||
? reportDetail.ReceiverFiles
|
||||
: reportDetail.WhistleblowerFiles;
|
||||
|
||||
var detailDate = TryFindReportFileDateUtc(entry, preferredFiles) ??
|
||||
TryFindReportFileDateUtc(entry, reportDetail.WhistleblowerFiles.Concat(reportDetail.ReceiverFiles));
|
||||
if (detailDate is not null)
|
||||
{
|
||||
return detailDate.Value;
|
||||
}
|
||||
}
|
||||
|
||||
return GetEntryFallbackDateUtc(entry);
|
||||
}
|
||||
|
||||
private static DateTime? TryFindReportFileDateUtc(ZipArchiveEntry entry, IEnumerable<ReportFileDto> reportFiles)
|
||||
{
|
||||
var entryName = NormalizeFileName(entry.Name);
|
||||
var candidates = reportFiles
|
||||
.Where(file => !string.IsNullOrWhiteSpace(file.Name) &&
|
||||
string.Equals(NormalizeFileName(file.Name), entryName, StringComparison.OrdinalIgnoreCase))
|
||||
.OrderByDescending(file => file.Size is null || file.Size == entry.Length)
|
||||
.ToList();
|
||||
|
||||
foreach (var candidate in candidates)
|
||||
{
|
||||
var parsed = TryParseGlobalLeaksDateUtc(candidate.CreationDate);
|
||||
if (parsed is not null)
|
||||
{
|
||||
return parsed;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private static DateTime? TryParseGlobalLeaksDateUtc(string? value)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(value))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var normalized = value.Replace("Z", "+00:00", StringComparison.Ordinal);
|
||||
return DateTimeOffset.TryParse(
|
||||
normalized,
|
||||
CultureInfo.InvariantCulture,
|
||||
DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal,
|
||||
out var parsed)
|
||||
? parsed.UtcDateTime
|
||||
: null;
|
||||
}
|
||||
|
||||
private static DateTime GetEntryFallbackDateUtc(ZipArchiveEntry entry)
|
||||
{
|
||||
var utcDate = entry.LastWriteTime.UtcDateTime;
|
||||
return utcDate == DateTime.MinValue
|
||||
? DateTime.UtcNow
|
||||
: utcDate;
|
||||
}
|
||||
|
||||
private static DateTime NormalizeUtc(DateTime value)
|
||||
=> value.Kind switch
|
||||
{
|
||||
DateTimeKind.Utc => value,
|
||||
DateTimeKind.Local => value.ToUniversalTime(),
|
||||
_ => DateTime.SpecifyKind(value, DateTimeKind.Utc)
|
||||
};
|
||||
|
||||
private static string NormalizeFileName(string fileName)
|
||||
=> Path.GetFileName(fileName.Replace('\\', '/')).Trim();
|
||||
|
||||
private static bool IsBlockedAttachmentEntry(ZipArchiveEntry entry)
|
||||
{
|
||||
var extension = Path.GetExtension(entry.Name);
|
||||
@@ -667,7 +899,7 @@ public sealed class DenunciaInboxService
|
||||
builder.AppendLine();
|
||||
}
|
||||
|
||||
private sealed record ProcessZipResult(int ComplaintId, IReadOnlyList<string> Warnings);
|
||||
private sealed record ProcessPackageResult(int ComplaintId, int ImportedCount, IReadOnlyList<string> Warnings);
|
||||
|
||||
private sealed record ReadFilesResult(List<FicherosDenuncias> Files, IReadOnlyList<string> Warnings);
|
||||
}
|
||||
|
||||
@@ -121,6 +121,51 @@ public sealed class EncryptedDenunciaStore : IDenunciaStore, IFilteredDenunciaSt
|
||||
return await UnprotectAttachmentsAsync(ficheros, skipPurgedRows: false, cancellationToken);
|
||||
}
|
||||
|
||||
public Task<List<FicherosDenuncias>> GetFicherosMetadataByDenunciaAsync(
|
||||
int denunciaId,
|
||||
CancellationToken cancellationToken = default)
|
||||
=> _inner.GetFicherosMetadataByDenunciaAsync(denunciaId, cancellationToken);
|
||||
|
||||
public async Task<List<GestionaUploadHistoryEntry>> GetGestionaUploadHistoryAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
var history = await _inner.GetGestionaUploadHistoryAsync(cancellationToken);
|
||||
var knownIds = history
|
||||
.Select(item => item.DenunciaId)
|
||||
.ToHashSet();
|
||||
|
||||
List<GestionaUploadHistoryEntry> fallbackHistory = [];
|
||||
try
|
||||
{
|
||||
var storedComplaints = await _inner.GetDenunciasByScopeAsync(DenunciaListScope.InGestiona, cancellationToken);
|
||||
var fallbackComplaints = await UnprotectComplaintsAsync(storedComplaints, skipPurgedRows: true, cancellationToken);
|
||||
fallbackHistory = fallbackComplaints
|
||||
.Where(complaint => complaint.FechaSubidaAGestiona != DateTime.MinValue &&
|
||||
!knownIds.Contains(complaint.Id_Denuncia))
|
||||
.Select(complaint => new GestionaUploadHistoryEntry(
|
||||
0,
|
||||
complaint.Id_Denuncia,
|
||||
complaint.Expediente_Gestiona ?? string.Empty,
|
||||
complaint.ExpedienteGestionaMostrable,
|
||||
complaint.UltimaSubidaGestionaTipoMostrable,
|
||||
ExtractGroupCode(complaint.UltimoGrupoAsignadoGestionaMostrable),
|
||||
"No registrado",
|
||||
complaint.FechaSubidaAGestiona,
|
||||
complaint.NombreDenuncia ?? string.Empty,
|
||||
complaint.ArchivoElegido ?? string.Empty))
|
||||
.ToList();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogWarning(ex, "No se ha podido completar el historico de Gestiona con denuncias antiguas descifrables.");
|
||||
}
|
||||
|
||||
return history
|
||||
.Concat(fallbackHistory)
|
||||
.OrderByDescending(item => item.UploadedAtUtc)
|
||||
.ThenByDescending(item => item.Id)
|
||||
.ToList();
|
||||
}
|
||||
|
||||
private async Task<List<FicherosDenuncias>> UnprotectAttachmentsAsync(
|
||||
List<FicherosDenuncias> ficheros,
|
||||
bool skipPurgedRows,
|
||||
@@ -153,6 +198,14 @@ public sealed class EncryptedDenunciaStore : IDenunciaStore, IFilteredDenunciaSt
|
||||
return denuncia is null ? null : await UnprotectComplaintAsync(denuncia, [], cancellationToken);
|
||||
}
|
||||
|
||||
public async Task<DenunciasGestiona?> GetDenunciaByGestionaFileCodeAsync(
|
||||
string gestionaFileCode,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var denuncia = await _inner.GetDenunciaByGestionaFileCodeAsync(gestionaFileCode, cancellationToken);
|
||||
return denuncia is null ? null : await UnprotectComplaintAsync(denuncia, [], cancellationToken);
|
||||
}
|
||||
|
||||
public async Task UpsertDenunciaAsync(DenunciasGestiona denuncia, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var key = await _envelopeKeyProvider.GetCurrentKeyAsync(cancellationToken);
|
||||
@@ -165,6 +218,12 @@ public sealed class EncryptedDenunciaStore : IDenunciaStore, IFilteredDenunciaSt
|
||||
await _inner.UpsertFicherosAsync(ficheros.Select(fichero => ProtectAttachment(fichero, key)).ToArray(), cancellationToken);
|
||||
}
|
||||
|
||||
public Task AddGestionaUploadHistoryAsync(
|
||||
GestionaUploadHistoryCreateRequest request,
|
||||
string username,
|
||||
CancellationToken cancellationToken = default)
|
||||
=> _inner.AddGestionaUploadHistoryAsync(request, username, cancellationToken);
|
||||
|
||||
public Task MarkFicherosAsUploadedAsync(
|
||||
int denunciaId,
|
||||
IEnumerable<string> fileNames,
|
||||
@@ -219,6 +278,9 @@ public sealed class EncryptedDenunciaStore : IDenunciaStore, IFilteredDenunciaSt
|
||||
EstadoDenuncia = source.EstadoDenuncia,
|
||||
ArchivoElegido = source.ArchivoElegido,
|
||||
FechaSubidaAGestiona = source.FechaSubidaAGestiona,
|
||||
UltimaSubidaGestionaTipo = source.UltimaSubidaGestionaTipo,
|
||||
UltimoGrupoAsignadoGestiona = source.UltimoGrupoAsignadoGestiona,
|
||||
PendingUpdateSource = source.PendingUpdateSource,
|
||||
EnGestiona = source.EnGestiona,
|
||||
EnRechazada = source.EnRechazada,
|
||||
|
||||
@@ -252,6 +314,18 @@ public sealed class EncryptedDenunciaStore : IDenunciaStore, IFilteredDenunciaSt
|
||||
return rebuilt;
|
||||
}
|
||||
|
||||
private static string ExtractGroupCode(string? value)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(value))
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
var trimmed = value.Trim();
|
||||
var code = new string(trimmed.TakeWhile(char.IsDigit).ToArray());
|
||||
return string.IsNullOrWhiteSpace(code) ? trimmed : code;
|
||||
}
|
||||
|
||||
private static DenunciasGestiona? TryParseStoredReport(DenunciasGestiona stored)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(stored.TextoOriginalReport))
|
||||
@@ -286,6 +360,9 @@ public sealed class EncryptedDenunciaStore : IDenunciaStore, IFilteredDenunciaSt
|
||||
target.EstadoDenuncia = stored.EstadoDenuncia;
|
||||
target.ArchivoElegido = stored.ArchivoElegido;
|
||||
target.FechaSubidaAGestiona = stored.FechaSubidaAGestiona;
|
||||
target.UltimaSubidaGestionaTipo = stored.UltimaSubidaGestionaTipo;
|
||||
target.UltimoGrupoAsignadoGestiona = stored.UltimoGrupoAsignadoGestiona;
|
||||
target.PendingUpdateSource = stored.PendingUpdateSource;
|
||||
target.EnGestiona = stored.EnGestiona;
|
||||
target.EnRechazada = stored.EnRechazada;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
using System.Net.Http.Headers;
|
||||
using System.Security.Cryptography;
|
||||
using System.Globalization;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using GestionaDenuncias.Shared.Models;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace ApiDenuncias.Services;
|
||||
@@ -35,6 +37,17 @@ public sealed class GestionaDocumentWorkflowService
|
||||
_configuration["Gestiona:AccessToken"]
|
||||
?? throw new InvalidOperationException("Falta Gestiona:AccessToken en appsettings.");
|
||||
|
||||
private string DocumentMetadataLanguage =>
|
||||
_configuration["Gestiona:DocumentMetadataLanguage"] ?? "es";
|
||||
|
||||
private string DocumentMetadataType =>
|
||||
_configuration["Gestiona:DocumentMetadataType"]
|
||||
?? throw new InvalidOperationException("Falta Gestiona:DocumentMetadataType para informar el tipo documental.");
|
||||
|
||||
private string DocumentMetadataSubtype =>
|
||||
_configuration["Gestiona:DocumentMetadataSubtype"]
|
||||
?? throw new InvalidOperationException("Falta Gestiona:DocumentMetadataSubtype para informar el subtipo documental.");
|
||||
|
||||
public async Task<string> UploadDocumentAndReturnUrlAsync(string fileUrl, byte[] contentBytes, string fileName)
|
||||
{
|
||||
var fileUrlAbs = EnsureAbsoluteGestionaUrl(fileUrl, GestionaApiBase);
|
||||
@@ -47,7 +60,9 @@ public sealed class GestionaDocumentWorkflowService
|
||||
name = fileName,
|
||||
description = "Documento de denuncia",
|
||||
elaboration_state = "EE01",
|
||||
metadata_language = "ES",
|
||||
metadata_language = DocumentMetadataLanguage,
|
||||
metadata_type = DocumentMetadataType,
|
||||
metadata_subtype = DocumentMetadataSubtype,
|
||||
links = new[] { new { rel = "content", href = uploadUri } }
|
||||
};
|
||||
|
||||
@@ -106,28 +121,33 @@ public sealed class GestionaDocumentWorkflowService
|
||||
throw new InvalidOperationException("No se pudo obtener la URL del documento creado en Gestiona.");
|
||||
}
|
||||
|
||||
public async Task TramitarDocumentoAsync(string documentUrl, string assignedGroupHref, int? complaintId = null)
|
||||
public async Task TramitarDocumentoAsync(
|
||||
string documentUrl,
|
||||
string assignedGroupCode,
|
||||
int? complaintId = null,
|
||||
bool isUpdate = false,
|
||||
string? updateSource = null)
|
||||
{
|
||||
_ = assignedGroupHref;
|
||||
|
||||
var docUrlAbs = EnsureAbsoluteGestionaUrl(documentUrl, GestionaApiBase);
|
||||
var templateHref = GetConfiguredTemplateHref(docUrlAbs);
|
||||
|
||||
if (string.IsNullOrWhiteSpace(templateHref))
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
"Falta Gestiona:CircuitTemplateId. No se listan plantillas para evitar campos deprecated.");
|
||||
}
|
||||
|
||||
var payload = await GetCircuitTemplatePayloadAsync(templateHref);
|
||||
var operationLabel = ComplaintUpdateSources.IsReceiver(updateSource)
|
||||
? $"comunicacion OAAF grupo {NormalizeGroupCode(assignedGroupCode)}"
|
||||
: isUpdate
|
||||
? $"actualizacion grupo {NormalizeGroupCode(assignedGroupCode)}"
|
||||
: "nueva denuncia";
|
||||
var (templateHref, payload) = await ResolveCircuitTemplatePayloadAsync(
|
||||
docUrlAbs,
|
||||
isUpdate,
|
||||
assignedGroupCode,
|
||||
updateSource);
|
||||
var (success, statusCode, body) = await TryPostCircuitAsync(docUrlAbs, payload);
|
||||
if (success)
|
||||
{
|
||||
_logger.LogInformation(
|
||||
"Documento {DocumentUrl} enviado a circuito con plantilla {TemplateHref}. Denuncia={ComplaintId}.",
|
||||
"Documento {DocumentUrl} enviado a circuito con plantilla {TemplateHref}. Denuncia={ComplaintId}. Tipo={OperationType}.",
|
||||
docUrlAbs,
|
||||
templateHref,
|
||||
complaintId);
|
||||
complaintId,
|
||||
operationLabel);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -142,6 +162,194 @@ public sealed class GestionaDocumentWorkflowService
|
||||
$"TramitarDocumentoAsync: {(int)statusCode} {statusCode}\n{body}");
|
||||
}
|
||||
|
||||
private async Task<(string TemplateHref, string Payload)> ResolveCircuitTemplatePayloadAsync(
|
||||
string documentUrl,
|
||||
bool isUpdate,
|
||||
string? assignedGroupCode,
|
||||
string? updateSource)
|
||||
{
|
||||
var templatesUrl = $"{documentUrl.TrimEnd('/')}/circuit/templates";
|
||||
var templates = await GetCircuitTemplatesAsync(templatesUrl);
|
||||
if (templates.Count == 0)
|
||||
{
|
||||
throw new InvalidOperationException("Gestiona no ha devuelto plantillas de circuito para el documento.");
|
||||
}
|
||||
|
||||
var selection = GetOperationCircuitTemplateSelection(isUpdate, assignedGroupCode, updateSource);
|
||||
if (!string.IsNullOrWhiteSpace(selection.TemplateName))
|
||||
{
|
||||
var resolved = await TryResolveCircuitTemplatePayloadByNameAsync(templates, selection.TemplateName);
|
||||
if (resolved is not null)
|
||||
{
|
||||
return resolved.Value;
|
||||
}
|
||||
|
||||
if (selection.Required)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"No se ha encontrado la plantilla de circuito '{selection.TemplateName}' para {selection.OperationLabel}.");
|
||||
}
|
||||
}
|
||||
|
||||
var configuredName = _configuration["Gestiona:CircuitTemplateName"];
|
||||
var configuredResolved = await TryResolveCircuitTemplatePayloadByNameAsync(templates, configuredName);
|
||||
if (configuredResolved is not null)
|
||||
{
|
||||
return configuredResolved.Value;
|
||||
}
|
||||
|
||||
var signerTitle = _configuration["Gestiona:CircuitSignerStampTitle"];
|
||||
if (!string.IsNullOrWhiteSpace(signerTitle))
|
||||
{
|
||||
foreach (var template in templates)
|
||||
{
|
||||
var href = GetRequiredSelfHref(template);
|
||||
var payload = await GetCircuitTemplatePayloadAsync(href);
|
||||
if (payload.Contains(signerTitle, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return (href, payload);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (templates.Count == 1)
|
||||
{
|
||||
var href = GetRequiredSelfHref(templates[0]);
|
||||
return (href, await GetCircuitTemplatePayloadAsync(href));
|
||||
}
|
||||
|
||||
throw new InvalidOperationException(
|
||||
"No se puede seleccionar de forma inequivoca la plantilla de circuito. Configura Gestiona:CircuitTemplateName o Gestiona:CircuitSignerStampTitle.");
|
||||
}
|
||||
|
||||
private CircuitTemplateSelection GetOperationCircuitTemplateSelection(
|
||||
bool isUpdate,
|
||||
string? assignedGroupCode,
|
||||
string? updateSource)
|
||||
{
|
||||
if (!isUpdate)
|
||||
{
|
||||
return new CircuitTemplateSelection(
|
||||
_configuration["Gestiona:CircuitNewComplaintTemplateName"],
|
||||
Required: true,
|
||||
OperationLabel: "nueva denuncia");
|
||||
}
|
||||
|
||||
if (ComplaintUpdateSources.IsReceiver(updateSource))
|
||||
{
|
||||
return NormalizeGroupCode(assignedGroupCode) switch
|
||||
{
|
||||
"510" => new CircuitTemplateSelection(
|
||||
_configuration["Gestiona:CircuitCommunicationSdiTemplateName"],
|
||||
Required: true,
|
||||
OperationLabel: "comunicacion SDI a denunciante"),
|
||||
|
||||
"600" => new CircuitTemplateSelection(
|
||||
_configuration["Gestiona:CircuitCommunicationSajTemplateName"],
|
||||
Required: true,
|
||||
OperationLabel: "comunicacion SAJ a denunciante"),
|
||||
|
||||
_ => throw new InvalidOperationException(
|
||||
"Las comunicaciones de la OAAF solo pueden tramitarse con los grupos 510 o 600.")
|
||||
};
|
||||
}
|
||||
|
||||
var defaultUpdateTemplateName = FirstConfigured(
|
||||
_configuration["Gestiona:CircuitTemplateName"],
|
||||
_configuration["Gestiona:CircuitUpdateTemplateName"]);
|
||||
|
||||
return NormalizeGroupCode(assignedGroupCode) switch
|
||||
{
|
||||
"510" => new CircuitTemplateSelection(
|
||||
FirstConfigured(
|
||||
_configuration["Gestiona:CircuitUpdateSdiTemplateName"],
|
||||
_configuration["Gestiona:CircuitUpdateTemplateNameSdi"]),
|
||||
Required: true,
|
||||
OperationLabel: "actualizacion de denuncia SDI"),
|
||||
|
||||
"600" => new CircuitTemplateSelection(
|
||||
FirstConfigured(
|
||||
_configuration["Gestiona:CircuitUpdateSajTemplateName"],
|
||||
_configuration["Gestiona:CircuitUpdateTemplateNameSaj"]),
|
||||
Required: true,
|
||||
OperationLabel: "actualizacion de denuncia SAJ"),
|
||||
|
||||
_ => new CircuitTemplateSelection(
|
||||
defaultUpdateTemplateName,
|
||||
Required: !string.IsNullOrWhiteSpace(defaultUpdateTemplateName),
|
||||
OperationLabel: "actualizacion de denuncia")
|
||||
};
|
||||
}
|
||||
|
||||
private static string? FirstConfigured(params string?[] values)
|
||||
=> values.FirstOrDefault(value => !string.IsNullOrWhiteSpace(value));
|
||||
|
||||
private static string NormalizeGroupCode(string? assignedGroupCode)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(assignedGroupCode))
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
var digits = new string(assignedGroupCode.Where(char.IsDigit).ToArray());
|
||||
return string.IsNullOrWhiteSpace(digits) ? assignedGroupCode.Trim() : digits;
|
||||
}
|
||||
|
||||
private sealed record CircuitTemplateSelection(string? TemplateName, bool Required, string OperationLabel);
|
||||
|
||||
private async Task<(string TemplateHref, string Payload)?> TryResolveCircuitTemplatePayloadByNameAsync(
|
||||
List<JsonElement> templates,
|
||||
string? templateName)
|
||||
{
|
||||
var configuredName = NormalizeKey(templateName);
|
||||
if (string.IsNullOrWhiteSpace(configuredName))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
foreach (var template in templates)
|
||||
{
|
||||
if (NormalizeKey(GetJsonString(template, "name")).Contains(configuredName, StringComparison.Ordinal) ||
|
||||
NormalizeKey(GetJsonString(template, "title")).Contains(configuredName, StringComparison.Ordinal))
|
||||
{
|
||||
var href = GetRequiredSelfHref(template);
|
||||
return (href, await GetCircuitTemplatePayloadAsync(href));
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private async Task<List<JsonElement>> GetCircuitTemplatesAsync(string templatesUrl)
|
||||
{
|
||||
using var req = new HttpRequestMessage(HttpMethod.Get, templatesUrl);
|
||||
req.Headers.TryAddWithoutValidation("X-Gestiona-Access-Token", GestionaAccessToken);
|
||||
req.Headers.Accept.Add(
|
||||
MediaTypeWithQualityHeaderValue.Parse("application/vnd.gestiona.circuits.templates-filedoc-page+json"));
|
||||
|
||||
using var resp = await CreateRawHttp().SendAsync(req);
|
||||
LogDeprecatedHeaders(resp, "GET plantillas circuito Gestiona");
|
||||
var body = await resp.Content.ReadAsStringAsync();
|
||||
if (!resp.IsSuccessStatusCode)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"GetCircuitTemplatesAsync: {(int)resp.StatusCode} {resp.StatusCode}\n{body}");
|
||||
}
|
||||
|
||||
using var doc = JsonDocument.Parse(body);
|
||||
var source = doc.RootElement.TryGetProperty("content", out var content) &&
|
||||
content.ValueKind == JsonValueKind.Array
|
||||
? content
|
||||
: doc.RootElement;
|
||||
|
||||
if (source.ValueKind != JsonValueKind.Array)
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
return source.EnumerateArray().Select(item => item.Clone()).ToList();
|
||||
}
|
||||
|
||||
private async Task<string> GetCircuitTemplatePayloadAsync(string templateHref)
|
||||
{
|
||||
using var req = new HttpRequestMessage(HttpMethod.Get, templateHref);
|
||||
@@ -182,14 +390,6 @@ public sealed class GestionaDocumentWorkflowService
|
||||
return (resp.IsSuccessStatusCode, resp.StatusCode, body);
|
||||
}
|
||||
|
||||
private string? GetConfiguredTemplateHref(string documentUrl)
|
||||
{
|
||||
var templateId = _configuration["Gestiona:CircuitTemplateId"];
|
||||
return string.IsNullOrWhiteSpace(templateId)
|
||||
? null
|
||||
: $"{documentUrl.TrimEnd('/')}/circuit/templates/{templateId.Trim()}";
|
||||
}
|
||||
|
||||
private HttpClient CreateRawHttp() => _httpClientFactory.CreateClient();
|
||||
|
||||
private async Task<string> CreateUploadAsync(byte[] contentBytes, string fileName)
|
||||
@@ -279,6 +479,65 @@ public sealed class GestionaDocumentWorkflowService
|
||||
return BitConverter.ToString(hash).Replace("-", "").ToLowerInvariant();
|
||||
}
|
||||
|
||||
private static string GetRequiredSelfHref(JsonElement item)
|
||||
{
|
||||
return GetLinkHref(item, "self")
|
||||
?? throw new InvalidOperationException("La plantilla de circuito no contiene link 'self'.");
|
||||
}
|
||||
|
||||
private static string? GetLinkHref(JsonElement item, string rel)
|
||||
{
|
||||
if (!item.TryGetProperty("links", out var links) || links.ValueKind != JsonValueKind.Array)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
foreach (var link in links.EnumerateArray())
|
||||
{
|
||||
if (link.TryGetProperty("rel", out var relProp) &&
|
||||
string.Equals(relProp.GetString(), rel, StringComparison.OrdinalIgnoreCase) &&
|
||||
link.TryGetProperty("href", out var hrefProp) &&
|
||||
hrefProp.ValueKind == JsonValueKind.String)
|
||||
{
|
||||
return hrefProp.GetString();
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private static string? GetJsonString(JsonElement item, string propertyName)
|
||||
{
|
||||
return item.TryGetProperty(propertyName, out var property) &&
|
||||
property.ValueKind == JsonValueKind.String
|
||||
? property.GetString()
|
||||
: null;
|
||||
}
|
||||
|
||||
private static string NormalizeKey(string? value)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(value))
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
var normalized = value.Normalize(NormalizationForm.FormD);
|
||||
var builder = new StringBuilder(normalized.Length);
|
||||
|
||||
foreach (var character in normalized)
|
||||
{
|
||||
var category = CharUnicodeInfo.GetUnicodeCategory(character);
|
||||
if (category == UnicodeCategory.NonSpacingMark)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
builder.Append(char.IsLetterOrDigit(character) ? char.ToUpperInvariant(character) : ' ');
|
||||
}
|
||||
|
||||
return string.Join(' ', builder.ToString().Split(' ', StringSplitOptions.RemoveEmptyEntries));
|
||||
}
|
||||
|
||||
private void LogDeprecatedHeaders(HttpResponseMessage response, string operation)
|
||||
{
|
||||
if (response.Headers.TryGetValues("X-Gestiona-Deprecated", out var deprecated))
|
||||
|
||||
@@ -0,0 +1,135 @@
|
||||
using System.Globalization;
|
||||
using GestionaDenuncias.Shared.Models;
|
||||
using MySqlConnector;
|
||||
|
||||
namespace ApiDenuncias.Services;
|
||||
|
||||
public sealed class GestionaExpedienteExceptionStore
|
||||
{
|
||||
private readonly MySqlConnectionStringProvider _connectionStringProvider;
|
||||
|
||||
public GestionaExpedienteExceptionStore(MySqlConnectionStringProvider connectionStringProvider)
|
||||
{
|
||||
_connectionStringProvider = connectionStringProvider;
|
||||
}
|
||||
|
||||
public async Task EnsureSchemaAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
await using var connection = await OpenConnectionAsync(cancellationToken);
|
||||
await EnsureTableAsync(connection, cancellationToken);
|
||||
}
|
||||
|
||||
public async Task<GestionaExpedienteException?> FindAsync(
|
||||
DenunciasGestiona denuncia,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
await using var connection = await OpenConnectionAsync(cancellationToken);
|
||||
await EnsureTableAsync(connection, cancellationToken);
|
||||
|
||||
var references = BuildReferences(denuncia);
|
||||
await using var command = connection.CreateCommand();
|
||||
command.Parameters.AddWithValue("@externalReportId", denuncia.Id_Denuncia);
|
||||
|
||||
var referenceFilter = string.Empty;
|
||||
if (references.Count > 0)
|
||||
{
|
||||
var parameterNames = new List<string>(references.Count);
|
||||
for (var i = 0; i < references.Count; i++)
|
||||
{
|
||||
var parameterName = $"@reference{i}";
|
||||
parameterNames.Add(parameterName);
|
||||
command.Parameters.AddWithValue(parameterName, references[i]);
|
||||
}
|
||||
|
||||
referenceFilter = $" OR external_reference IN ({string.Join(", ", parameterNames)})";
|
||||
}
|
||||
|
||||
command.CommandText = $"""
|
||||
SELECT external_report_id, external_reference, gestiona_file_code
|
||||
FROM gestiona_file_exceptions
|
||||
WHERE external_report_id = @externalReportId
|
||||
{referenceFilter}
|
||||
ORDER BY
|
||||
CASE WHEN external_report_id = @externalReportId THEN 0 ELSE 1 END,
|
||||
source_row DESC,
|
||||
id DESC
|
||||
LIMIT 1;
|
||||
""";
|
||||
|
||||
await using var reader = await command.ExecuteReaderAsync(cancellationToken);
|
||||
if (!await reader.ReadAsync(cancellationToken))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return new GestionaExpedienteException(
|
||||
reader.IsDBNull(reader.GetOrdinal("external_report_id"))
|
||||
? null
|
||||
: reader.GetInt32(reader.GetOrdinal("external_report_id")),
|
||||
reader.IsDBNull(reader.GetOrdinal("external_reference"))
|
||||
? null
|
||||
: reader.GetString(reader.GetOrdinal("external_reference")),
|
||||
reader.GetString(reader.GetOrdinal("gestiona_file_code")));
|
||||
}
|
||||
|
||||
private static List<string> BuildReferences(DenunciasGestiona denuncia)
|
||||
{
|
||||
var references = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
if (denuncia.Id_RegistroDenuncia > 0)
|
||||
{
|
||||
references.Add(denuncia.Id_RegistroDenuncia.ToString(CultureInfo.InvariantCulture));
|
||||
}
|
||||
|
||||
AddReference(references, denuncia.Etiqueta);
|
||||
AddReference(references, denuncia.Asunto);
|
||||
|
||||
return references.ToList();
|
||||
}
|
||||
|
||||
private static void AddReference(HashSet<string> references, string? value)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(value))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
references.Add(value.Trim());
|
||||
}
|
||||
|
||||
private async Task<MySqlConnection> OpenConnectionAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
var connectionString = await _connectionStringProvider.GetConnectionStringAsync(cancellationToken);
|
||||
var connection = new MySqlConnection(connectionString);
|
||||
await connection.OpenAsync(cancellationToken);
|
||||
return connection;
|
||||
}
|
||||
|
||||
private static async Task EnsureTableAsync(MySqlConnection connection, CancellationToken cancellationToken)
|
||||
{
|
||||
await using var command = new MySqlCommand(
|
||||
"""
|
||||
CREATE TABLE IF NOT EXISTS gestiona_file_exceptions (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
external_report_id INT NULL,
|
||||
external_reference VARCHAR(100) NOT NULL,
|
||||
gestiona_file_code VARCHAR(50) NOT NULL,
|
||||
notes VARCHAR(255) NULL,
|
||||
source_row INT NULL,
|
||||
created_at_utc DATETIME(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6),
|
||||
updated_at_utc DATETIME(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6) ON UPDATE CURRENT_TIMESTAMP(6),
|
||||
UNIQUE KEY uq_gestiona_file_exceptions_ref_code (external_reference, gestiona_file_code),
|
||||
KEY ix_gestiona_file_exceptions_report (external_report_id),
|
||||
KEY ix_gestiona_file_exceptions_reference (external_reference),
|
||||
KEY ix_gestiona_file_exceptions_code (gestiona_file_code)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
|
||||
""",
|
||||
connection);
|
||||
await command.ExecuteNonQueryAsync(cancellationToken);
|
||||
}
|
||||
}
|
||||
|
||||
public sealed record GestionaExpedienteException(
|
||||
int? ExternalReportId,
|
||||
string? ExternalReference,
|
||||
string GestionaFileCode);
|
||||
@@ -12,6 +12,7 @@ using System.Text;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace ApiDenuncias.Services
|
||||
@@ -106,17 +107,12 @@ namespace ApiDenuncias.Services
|
||||
// Expedientes (file)
|
||||
// =========================================================
|
||||
|
||||
public async Task<GestionaCreateFileResponse> CreateFileAsync(Guid procedureId, string subject, string documentSeries, string siaCode)
|
||||
public async Task<GestionaCreateFileResponse> CreateFileAsync(string subject, string documentSeries, string siaCode)
|
||||
{
|
||||
_ = subject;
|
||||
_ = documentSeries;
|
||||
_ = siaCode;
|
||||
|
||||
var effectiveProcedureId = procedureId == Guid.Empty
|
||||
? Guid.Parse("82722c9b-cecc-4299-8a7b-ce5abeb8170b")
|
||||
: procedureId;
|
||||
|
||||
var url = await ResolveExternalProcedureCreateFileUrlAsync(effectiveProcedureId);
|
||||
var url = await ResolveExternalProcedureCreateFileUrlAsync(siaCode);
|
||||
using var req = new HttpRequestMessage(HttpMethod.Post, url);
|
||||
req.Headers.Accept.Clear();
|
||||
req.Headers.Accept.Add(
|
||||
@@ -138,24 +134,263 @@ namespace ApiDenuncias.Services
|
||||
return new GestionaCreateFileResponse(fileUrl, fileOpenUrl);
|
||||
}
|
||||
|
||||
private Task<string> ResolveExternalProcedureCreateFileUrlAsync(Guid procedureId)
|
||||
private async Task<string> ResolveExternalProcedureCreateFileUrlAsync(string siaCode)
|
||||
{
|
||||
var externalProcedureId = Guid.TryParse(_opts.ExternalProcedureId, out var configuredExternalProcedureId)
|
||||
? configuredExternalProcedureId
|
||||
: procedureId;
|
||||
var proceduresUrl = await ResolveRootLinkHrefAsync("vnd.gestiona.catalog-2015")
|
||||
?? "/rest/catalog-2015/procedures";
|
||||
var procedures = await GetContentArrayAsync(
|
||||
proceduresUrl,
|
||||
"application/vnd.gestiona.procedures-2015-page+json",
|
||||
"GET catalogo de procedimientos Gestiona");
|
||||
|
||||
return Task.FromResult(
|
||||
$"/rest/catalog-2015/procedures/{procedureId}/external-procedures/{externalProcedureId}/create-file");
|
||||
var procedure = SelectProcedure(procedures, siaCode);
|
||||
var externalProceduresUrl = GetLinkHref(procedure, "external-procedures")
|
||||
?? throw new InvalidOperationException(
|
||||
"El procedimiento seleccionado no contiene link 'external-procedures'.");
|
||||
var externalProcedures = await GetContentArrayAsync(
|
||||
externalProceduresUrl,
|
||||
"application/vnd.gestiona.external-procedures-2015-page+json",
|
||||
"GET procedimientos externos Gestiona");
|
||||
|
||||
var externalProcedure = SelectExternalProcedure(externalProcedures, procedure, siaCode);
|
||||
return GetLinkHref(externalProcedure, "create-file")
|
||||
?? throw new InvalidOperationException(
|
||||
"El procedimiento externo seleccionado no contiene link 'create-file'.");
|
||||
}
|
||||
|
||||
private async Task<string?> ResolveRootLinkHrefAsync(string rel)
|
||||
{
|
||||
using var req = new HttpRequestMessage(HttpMethod.Get, "/rest");
|
||||
AddBasicHeaders(req);
|
||||
|
||||
using var resp = await _http.SendAsync(req);
|
||||
LogDeprecatedHeaders(resp, "GET /rest");
|
||||
var body = await resp.Content.ReadAsStringAsync();
|
||||
if (!resp.IsSuccessStatusCode)
|
||||
{
|
||||
throw new InvalidOperationException($"ResolveRootLinkHrefAsync: {(int)resp.StatusCode} {resp.StatusCode}\n{body}");
|
||||
}
|
||||
|
||||
using var doc = JsonDocument.Parse(body);
|
||||
return ToAbsoluteGestionaHref(GetLinkHref(doc.RootElement, rel));
|
||||
}
|
||||
|
||||
private async Task<List<JsonElement>> GetContentArrayAsync(string url, string accept, string operation)
|
||||
{
|
||||
using var req = new HttpRequestMessage(HttpMethod.Get, url);
|
||||
req.Headers.TryAddWithoutValidation("X-Gestiona-Access-Token", _opts.AccessToken);
|
||||
req.Headers.Accept.Clear();
|
||||
req.Headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse(accept));
|
||||
|
||||
using var resp = await _http.SendAsync(req);
|
||||
LogDeprecatedHeaders(resp, operation);
|
||||
var body = await resp.Content.ReadAsStringAsync();
|
||||
if (!resp.IsSuccessStatusCode)
|
||||
{
|
||||
throw new InvalidOperationException($"{operation}: {(int)resp.StatusCode} {resp.StatusCode}\n{body}");
|
||||
}
|
||||
|
||||
using var doc = JsonDocument.Parse(body);
|
||||
var source = doc.RootElement.TryGetProperty("content", out var content) &&
|
||||
content.ValueKind == JsonValueKind.Array
|
||||
? content
|
||||
: doc.RootElement;
|
||||
|
||||
if (source.ValueKind != JsonValueKind.Array)
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
return source.EnumerateArray().Select(item => item.Clone()).ToList();
|
||||
}
|
||||
|
||||
private JsonElement SelectProcedure(IReadOnlyList<JsonElement> procedures, string siaCode)
|
||||
{
|
||||
if (procedures.Count == 0)
|
||||
{
|
||||
throw new InvalidOperationException("Gestiona no ha devuelto procedimientos en el catalogo.");
|
||||
}
|
||||
|
||||
var configuredName = NormalizeKey(_opts.ProcedureName);
|
||||
if (!string.IsNullOrWhiteSpace(configuredName))
|
||||
{
|
||||
var match = procedures.FirstOrDefault(item =>
|
||||
NormalizeKey(GetJsonString(item, "name")).Contains(configuredName, StringComparison.Ordinal) ||
|
||||
NormalizeKey(GetJsonString(item, "title")).Contains(configuredName, StringComparison.Ordinal) ||
|
||||
NormalizeKey(GetJsonString(item, "description")).Contains(configuredName, StringComparison.Ordinal));
|
||||
|
||||
if (match.ValueKind != JsonValueKind.Undefined)
|
||||
{
|
||||
return match;
|
||||
}
|
||||
|
||||
throw new InvalidOperationException(
|
||||
$"No se ha encontrado en Gestiona el procedimiento configurado '{_opts.ProcedureName}'. Procedimientos disponibles: {DescribeCatalogItems(procedures)}.");
|
||||
}
|
||||
|
||||
var normalizedSia = NormalizeKey(siaCode);
|
||||
if (!string.IsNullOrWhiteSpace(normalizedSia))
|
||||
{
|
||||
var match = procedures.FirstOrDefault(item =>
|
||||
NormalizeKey(GetJsonString(item, "sia_code")) == normalizedSia ||
|
||||
NormalizeKey(GetJsonString(item, "code")) == normalizedSia);
|
||||
|
||||
if (match.ValueKind != JsonValueKind.Undefined)
|
||||
{
|
||||
return match;
|
||||
}
|
||||
}
|
||||
|
||||
if (procedures.Count == 1)
|
||||
{
|
||||
return procedures[0];
|
||||
}
|
||||
|
||||
throw new InvalidOperationException(
|
||||
"Falta configurar Gestiona:ProcedureName para seleccionar el procedimiento sin usar IDs fijos.");
|
||||
}
|
||||
|
||||
private JsonElement SelectExternalProcedure(
|
||||
IReadOnlyList<JsonElement> externalProcedures,
|
||||
JsonElement selectedProcedure,
|
||||
string siaCode)
|
||||
{
|
||||
var withCreateFile = externalProcedures
|
||||
.Where(item => !string.IsNullOrWhiteSpace(GetLinkHref(item, "create-file")))
|
||||
.ToList();
|
||||
|
||||
if (withCreateFile.Count == 0)
|
||||
{
|
||||
throw new InvalidOperationException("El procedimiento seleccionado no tiene procedimientos externos con link 'create-file'.");
|
||||
}
|
||||
|
||||
var configuredSia = NormalizeKey(
|
||||
string.IsNullOrWhiteSpace(_opts.ExternalProcedureSiaCode)
|
||||
? siaCode
|
||||
: _opts.ExternalProcedureSiaCode);
|
||||
if (!string.IsNullOrWhiteSpace(configuredSia))
|
||||
{
|
||||
var match = withCreateFile.FirstOrDefault(item =>
|
||||
NormalizeKey(GetJsonString(item, "sia_code")) == configuredSia ||
|
||||
NormalizeKey(GetJsonString(item, "code")) == configuredSia);
|
||||
|
||||
if (match.ValueKind != JsonValueKind.Undefined)
|
||||
{
|
||||
return match;
|
||||
}
|
||||
}
|
||||
|
||||
var configuredName = NormalizeKey(_opts.ExternalProcedureName);
|
||||
if (!string.IsNullOrWhiteSpace(configuredName))
|
||||
{
|
||||
var match = withCreateFile.FirstOrDefault(item =>
|
||||
NormalizeKey(GetJsonString(item, "name")).Contains(configuredName, StringComparison.Ordinal) ||
|
||||
NormalizeKey(GetJsonString(item, "title")).Contains(configuredName, StringComparison.Ordinal));
|
||||
|
||||
if (match.ValueKind != JsonValueKind.Undefined)
|
||||
{
|
||||
return match;
|
||||
}
|
||||
}
|
||||
|
||||
var procedureId = GetJsonString(selectedProcedure, "id");
|
||||
if (!string.IsNullOrWhiteSpace(procedureId))
|
||||
{
|
||||
var sameId = withCreateFile.FirstOrDefault(item =>
|
||||
string.Equals(GetJsonString(item, "id"), procedureId, StringComparison.OrdinalIgnoreCase));
|
||||
if (sameId.ValueKind != JsonValueKind.Undefined)
|
||||
{
|
||||
return sameId;
|
||||
}
|
||||
}
|
||||
|
||||
if (withCreateFile.Count == 1)
|
||||
{
|
||||
return withCreateFile[0];
|
||||
}
|
||||
|
||||
throw new InvalidOperationException(
|
||||
$"No se puede seleccionar de forma inequivoca el procedimiento externo. Configura Gestiona:ExternalProcedureName o Gestiona:ExternalProcedureSiaCode. Disponibles: {DescribeCatalogItems(withCreateFile)}.");
|
||||
}
|
||||
|
||||
private async Task<string> ResolveAssignableGroupHrefAsync(string groupCode)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(groupCode))
|
||||
{
|
||||
throw new InvalidOperationException("Debe indicarse el codigo funcional del grupo de Gestiona.");
|
||||
}
|
||||
|
||||
var groupsUrl = await ResolveRootLinkHrefAsync("vnd.gestiona.files.assignees.groups")
|
||||
?? "/rest/files/assignees/groups";
|
||||
var groups = await GetContentArrayAsync(
|
||||
groupsUrl,
|
||||
"application/vnd.gestiona.groups-page+json",
|
||||
"GET grupos asignables Gestiona");
|
||||
|
||||
var normalizedCode = NormalizeKey(groupCode);
|
||||
foreach (var group in groups)
|
||||
{
|
||||
var code = NormalizeKey(GetJsonString(group, "code"));
|
||||
var name = NormalizeKey(GetJsonString(group, "name"));
|
||||
var title = NormalizeKey(GetJsonString(group, "title"));
|
||||
|
||||
if (code == normalizedCode ||
|
||||
name == normalizedCode ||
|
||||
title == normalizedCode ||
|
||||
name.StartsWith(normalizedCode + " ", StringComparison.Ordinal) ||
|
||||
title.StartsWith(normalizedCode + " ", StringComparison.Ordinal))
|
||||
{
|
||||
var href = ToAbsoluteGestionaHref(GetLinkHref(group, "self"));
|
||||
if (!string.IsNullOrWhiteSpace(href))
|
||||
{
|
||||
return href!;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
throw new InvalidOperationException(
|
||||
$"No se ha encontrado en Gestiona el grupo asignable '{groupCode}'. Grupos disponibles: {DescribeCatalogItems(groups)}.");
|
||||
}
|
||||
|
||||
private string? ToAbsoluteGestionaHref(string? href)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(href))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
if (Uri.TryCreate(href, UriKind.Absolute, out _))
|
||||
{
|
||||
return href;
|
||||
}
|
||||
|
||||
return href.StartsWith("/", StringComparison.Ordinal)
|
||||
? $"{_opts.ApiBase.TrimEnd('/')}{href}"
|
||||
: href;
|
||||
}
|
||||
|
||||
private static string DescribeCatalogItems(IEnumerable<JsonElement> items)
|
||||
{
|
||||
var values = items
|
||||
.Select(item => FirstNonEmpty(
|
||||
GetJsonString(item, "name"),
|
||||
GetJsonString(item, "title"),
|
||||
GetJsonString(item, "code"),
|
||||
GetJsonString(item, "sia_code"),
|
||||
GetJsonString(item, "id")))
|
||||
.Where(value => !string.IsNullOrWhiteSpace(value))
|
||||
.Take(10)
|
||||
.ToList();
|
||||
|
||||
return values.Count == 0 ? "(sin nombres disponibles)" : string.Join("; ", values);
|
||||
}
|
||||
|
||||
public async Task OpenFileAsync(
|
||||
string fileUrl,
|
||||
string? fileOpenUrl,
|
||||
Guid managementUnitGroupId,
|
||||
Guid assignedGroupId,
|
||||
string assignedGroupCode,
|
||||
bool confidential,
|
||||
string freeTitle,
|
||||
string siaCode)
|
||||
string freeTitle)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(fileOpenUrl))
|
||||
{
|
||||
@@ -164,20 +399,24 @@ namespace ApiDenuncias.Services
|
||||
}
|
||||
|
||||
var url = fileOpenUrl;
|
||||
var assignedGroupHref = await ResolveAssignableGroupHrefAsync(assignedGroupCode);
|
||||
var managementGroupCode = string.IsNullOrWhiteSpace(_opts.ManagementUnitGroupCode)
|
||||
? "700"
|
||||
: _opts.ManagementUnitGroupCode.Trim();
|
||||
var managementGroupHref = await ResolveAssignableGroupHrefAsync(managementGroupCode);
|
||||
|
||||
var payload = new
|
||||
{
|
||||
free_title = freeTitle,
|
||||
location = siaCode,
|
||||
entry_date = DateTimeOffset.UtcNow.ToUnixTimeSeconds().ToString(),
|
||||
confidential,
|
||||
initial_assignation = new[]
|
||||
{
|
||||
new { rel = "group", href = $"{_opts.ApiBase}/rest/groups/{assignedGroupId}" }
|
||||
new { rel = "group", href = assignedGroupHref }
|
||||
},
|
||||
links = new[]
|
||||
{
|
||||
new { rel = "management-unit-group", href = $"{_opts.ApiBase}/rest/groups/{managementUnitGroupId}" }
|
||||
new { rel = "management-unit-group", href = managementGroupHref }
|
||||
}
|
||||
};
|
||||
|
||||
@@ -195,6 +434,67 @@ namespace ApiDenuncias.Services
|
||||
throw new InvalidOperationException($"OpenFileAsync: {(int)resp.StatusCode} {resp.StatusCode}\n{body}");
|
||||
}
|
||||
|
||||
public async Task AssignFileAsync(string fileUrl, string assignedGroupCode)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(fileUrl))
|
||||
{
|
||||
throw new InvalidOperationException("AssignFileAsync: falta la URL del expediente.");
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(assignedGroupCode))
|
||||
{
|
||||
throw new InvalidOperationException("AssignFileAsync: falta el grupo asignado.");
|
||||
}
|
||||
|
||||
var assigneesUrl = await ResolveFileAssigneesUrlAsync(fileUrl);
|
||||
var assignedGroupHref = await ResolveAssignableGroupHrefAsync(assignedGroupCode);
|
||||
var payload = new
|
||||
{
|
||||
links = new[]
|
||||
{
|
||||
new { rel = "group", href = assignedGroupHref }
|
||||
}
|
||||
};
|
||||
var json = JsonSerializer.Serialize(payload);
|
||||
using var content = new StringContent(json, Encoding.UTF8, "application/vnd.gestiona.links");
|
||||
|
||||
using var req = new HttpRequestMessage(HttpMethod.Put, assigneesUrl)
|
||||
{
|
||||
Content = content
|
||||
};
|
||||
req.Headers.TryAddWithoutValidation("X-Gestiona-Access-Token", _opts.AccessToken);
|
||||
req.Headers.Accept.Clear();
|
||||
req.Headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse("application/vnd.gestiona.links"));
|
||||
|
||||
using var resp = await _http.SendAsync(req);
|
||||
LogDeprecatedHeaders(resp, "PUT file assignees");
|
||||
var body = await resp.Content.ReadAsStringAsync();
|
||||
if (!resp.IsSuccessStatusCode)
|
||||
{
|
||||
throw new InvalidOperationException($"AssignFileAsync: {(int)resp.StatusCode} {resp.StatusCode}\n{body}");
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<string> ResolveFileAssigneesUrlAsync(string fileUrl)
|
||||
{
|
||||
var normalizedFileUrl = fileUrl.TrimEnd('/');
|
||||
using var req = new HttpRequestMessage(HttpMethod.Get, normalizedFileUrl);
|
||||
AddTokenAndAccept(req, "application/vnd.gestiona.file+json", "2");
|
||||
|
||||
using var resp = await _http.SendAsync(req);
|
||||
LogDeprecatedHeaders(resp, $"GET {normalizedFileUrl}");
|
||||
var body = await resp.Content.ReadAsStringAsync();
|
||||
if (!resp.IsSuccessStatusCode)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"ResolveFileAssigneesUrlAsync: {(int)resp.StatusCode} {resp.StatusCode}\n{body}");
|
||||
}
|
||||
|
||||
using var doc = JsonDocument.Parse(body);
|
||||
return ToAbsoluteGestionaHref(GetLinkHref(doc.RootElement, "assignees"))
|
||||
?? $"{normalizedFileUrl}/assignees";
|
||||
}
|
||||
|
||||
public async Task<Guid> CreateFolderAsync(string fileUrl, string folderName)
|
||||
{
|
||||
var endpoint = $"{fileUrl}/documents-and-folders";
|
||||
@@ -336,21 +636,32 @@ namespace ApiDenuncias.Services
|
||||
{
|
||||
var filtro = new
|
||||
{
|
||||
result = new { max_results = 25 },
|
||||
filter = new { nif }
|
||||
nif
|
||||
};
|
||||
var jsonFiltro = JsonSerializer.Serialize(filtro);
|
||||
var b64 = Convert.ToBase64String(Encoding.UTF8.GetBytes(jsonFiltro));
|
||||
var url = $"{_opts.ApiBase}/rest/thirds?filter-view={Uri.EscapeDataString(b64)}";
|
||||
|
||||
using var req = new HttpRequestMessage(HttpMethod.Get, url);
|
||||
using var req = new HttpRequestMessage(HttpMethod.Get, "/rest/thirds");
|
||||
req.Headers.TryAddWithoutValidation("X-Gestiona-Access-Token", _opts.AccessToken);
|
||||
req.Headers.TryAddWithoutValidation("Accept", "application/vnd.gestiona.thirds-page+json");
|
||||
req.Content = new StringContent(
|
||||
JsonSerializer.Serialize(filtro),
|
||||
Encoding.UTF8,
|
||||
"application/vnd.gestiona.filter.thirds+json");
|
||||
|
||||
using var resp = await _http.SendAsync(req);
|
||||
resp.EnsureSuccessStatusCode();
|
||||
var body = await resp.Content.ReadAsStringAsync();
|
||||
|
||||
if (resp.StatusCode == System.Net.HttpStatusCode.NoContent ||
|
||||
string.IsNullOrWhiteSpace(body))
|
||||
{
|
||||
return default;
|
||||
}
|
||||
|
||||
if (!resp.IsSuccessStatusCode)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"Error BuscarTerceroPorNifAsync: {(int)resp.StatusCode} {resp.ReasonPhrase}\n{body}");
|
||||
}
|
||||
|
||||
using var doc = JsonDocument.Parse(body);
|
||||
if (!doc.RootElement.TryGetProperty("content", out var content) || content.ValueKind != JsonValueKind.Array)
|
||||
return default;
|
||||
@@ -468,8 +779,13 @@ namespace ApiDenuncias.Services
|
||||
if (resp.StatusCode == System.Net.HttpStatusCode.NoContent)
|
||||
return new HashSet<string>(StringComparer.Ordinal);
|
||||
|
||||
resp.EnsureSuccessStatusCode();
|
||||
var body = await resp.Content.ReadAsStringAsync();
|
||||
if (!resp.IsSuccessStatusCode)
|
||||
throw new InvalidOperationException(
|
||||
$"Error ObtenerTercerosEnlazadosAsync: {(int)resp.StatusCode} {resp.ReasonPhrase}\n{body}");
|
||||
|
||||
if (string.IsNullOrWhiteSpace(body))
|
||||
return new HashSet<string>(StringComparer.Ordinal);
|
||||
|
||||
using var doc = JsonDocument.Parse(body);
|
||||
var set = new HashSet<string>(StringComparer.Ordinal);
|
||||
@@ -507,14 +823,18 @@ namespace ApiDenuncias.Services
|
||||
throw new InvalidOperationException($"Error EnlazarTerceroExistenteAsync: {resp.StatusCode}\n{body}");
|
||||
}
|
||||
|
||||
public async Task AsegurarTerceroYEnlazarAsync(string fileUrl, ThirdPartyIdentityData thirdParty)
|
||||
public async Task<GestionaEnsureThirdResponse> AsegurarTerceroYEnlazarAsync(string fileUrl, ThirdPartyIdentityData thirdParty)
|
||||
{
|
||||
if (thirdParty is null)
|
||||
throw new ArgumentNullException(nameof(thirdParty));
|
||||
|
||||
thirdParty = NormalizeThirdParty(thirdParty);
|
||||
var warnings = new List<string>();
|
||||
|
||||
if (string.IsNullOrWhiteSpace(thirdParty.DocumentId)) return;
|
||||
if (string.IsNullOrWhiteSpace(thirdParty.DocumentId))
|
||||
{
|
||||
return new GestionaEnsureThirdResponse(true, warnings);
|
||||
}
|
||||
|
||||
var encontrado = await BuscarTerceroPorNifAsync(thirdParty.DocumentId);
|
||||
|
||||
@@ -525,19 +845,175 @@ namespace ApiDenuncias.Services
|
||||
_logger.LogWarning(
|
||||
"Se omite la creacion/enlace del tercero en Gestiona para el expediente {FileUrl}: datos identificativos incompletos.",
|
||||
fileUrl);
|
||||
return;
|
||||
return new GestionaEnsureThirdResponse(true, warnings);
|
||||
}
|
||||
|
||||
encontrado = await CrearTerceroAsync(thirdParty);
|
||||
}
|
||||
else if (thirdParty.Address?.HasAnyValue == true)
|
||||
else
|
||||
{
|
||||
warnings.AddRange(await BuildThirdPartyDifferenceWarningsAsync(encontrado.SelfHref, thirdParty));
|
||||
if (thirdParty.Address?.HasAnyValue == true)
|
||||
{
|
||||
await TryEnsureThirdAddressAsync(encontrado.SelfHref, thirdParty.Address);
|
||||
}
|
||||
}
|
||||
|
||||
var yaEnlazados = await ObtenerTercerosEnlazadosAsync(fileUrl);
|
||||
if (!yaEnlazados.Contains(encontrado.SelfHref))
|
||||
await EnlazarTerceroExistenteAsync(fileUrl, encontrado.SelfHref);
|
||||
|
||||
return new GestionaEnsureThirdResponse(true, warnings);
|
||||
}
|
||||
|
||||
private async Task<List<string>> BuildThirdPartyDifferenceWarningsAsync(
|
||||
string thirdSelfHref,
|
||||
ThirdPartyIdentityData thirdParty)
|
||||
{
|
||||
var warnings = new List<string>();
|
||||
var details = await GetThirdPartyDetailsAsync(thirdSelfHref);
|
||||
if (details is null)
|
||||
{
|
||||
return warnings;
|
||||
}
|
||||
|
||||
var gestionaType = GetJsonString(details.Value, "type");
|
||||
var expectedType = thirdParty.IsLegalEntity ? "JURIDICAL" : "PHISIC";
|
||||
AddThirdDifferenceWarning(warnings, "tipo de tercero", expectedType, gestionaType, normalizeAsCode: true);
|
||||
|
||||
if (thirdParty.IsLegalEntity)
|
||||
{
|
||||
AddThirdDifferenceWarning(
|
||||
warnings,
|
||||
"razon social",
|
||||
thirdParty.BusinessName,
|
||||
GetJsonString(details.Value, "business_name"));
|
||||
}
|
||||
else
|
||||
{
|
||||
AddThirdDifferenceWarning(
|
||||
warnings,
|
||||
"nombre",
|
||||
thirdParty.FirstName,
|
||||
GetJsonString(details.Value, "first_name"));
|
||||
|
||||
var gestionaLastName = string.Join(
|
||||
' ',
|
||||
new[]
|
||||
{
|
||||
GetJsonString(details.Value, "first_surname"),
|
||||
GetJsonString(details.Value, "second_surname")
|
||||
}.Where(value => !string.IsNullOrWhiteSpace(value)));
|
||||
AddThirdDifferenceWarning(warnings, "apellidos", thirdParty.LastName, gestionaLastName);
|
||||
}
|
||||
|
||||
AddThirdDifferenceWarning(warnings, "email", thirdParty.Email, GetJsonString(details.Value, "email"));
|
||||
AddThirdDifferenceWarning(
|
||||
warnings,
|
||||
"pais del documento",
|
||||
NormalizeCountryCode(thirdParty.CountryCode),
|
||||
GetJsonString(details.Value, "nif_country"),
|
||||
normalizeAsCode: true);
|
||||
AddThirdDifferenceWarning(
|
||||
warnings,
|
||||
"canal de notificacion",
|
||||
BuildNotificationChannel(thirdParty),
|
||||
GetJsonString(details.Value, "notification_channel"),
|
||||
normalizeAsCode: true);
|
||||
|
||||
if (warnings.Count > 0)
|
||||
{
|
||||
warnings.Insert(
|
||||
0,
|
||||
"El tercero ya existia en Gestiona y no se han sustituido sus datos. Si alguno debe cambiarse, actualizalo manualmente en Gestiona.");
|
||||
}
|
||||
|
||||
return warnings;
|
||||
}
|
||||
|
||||
private async Task<JsonElement?> GetThirdPartyDetailsAsync(string thirdSelfHref)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(thirdSelfHref))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
using var req = new HttpRequestMessage(HttpMethod.Get, thirdSelfHref);
|
||||
req.Headers.TryAddWithoutValidation("X-Gestiona-Access-Token", _opts.AccessToken);
|
||||
req.Headers.Accept.Clear();
|
||||
req.Headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse("application/vnd.gestiona.third+json; version=3"));
|
||||
|
||||
using var resp = await _http.SendAsync(req);
|
||||
LogDeprecatedHeaders(resp, "GET third");
|
||||
if (!resp.IsSuccessStatusCode)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var body = await resp.Content.ReadAsStringAsync();
|
||||
if (string.IsNullOrWhiteSpace(body))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
using var doc = JsonDocument.Parse(body);
|
||||
return doc.RootElement.Clone();
|
||||
}
|
||||
|
||||
private static void AddThirdDifferenceWarning(
|
||||
List<string> warnings,
|
||||
string fieldName,
|
||||
string? complaintValue,
|
||||
string? gestionaValue,
|
||||
bool normalizeAsCode = false)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(complaintValue))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var left = normalizeAsCode
|
||||
? NormalizeKey(complaintValue)
|
||||
: NormalizeForComparison(complaintValue);
|
||||
var right = normalizeAsCode
|
||||
? NormalizeKey(gestionaValue)
|
||||
: NormalizeForComparison(gestionaValue);
|
||||
|
||||
if (string.Equals(left, right, StringComparison.Ordinal))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
warnings.Add(
|
||||
$"El campo {fieldName} no coincide con Gestiona (denuncia: '{FormatWarningValue(complaintValue)}'; Gestiona: '{FormatWarningValue(gestionaValue)}').");
|
||||
}
|
||||
|
||||
private static string NormalizeForComparison(string? value)
|
||||
{
|
||||
return string.Join(
|
||||
' ',
|
||||
(value ?? string.Empty)
|
||||
.Trim()
|
||||
.Split(' ', StringSplitOptions.RemoveEmptyEntries))
|
||||
.ToUpperInvariant();
|
||||
}
|
||||
|
||||
private static string FormatWarningValue(string? value)
|
||||
{
|
||||
var normalized = string.Join(
|
||||
' ',
|
||||
(value ?? string.Empty)
|
||||
.Trim()
|
||||
.Split(' ', StringSplitOptions.RemoveEmptyEntries));
|
||||
if (string.IsNullOrWhiteSpace(normalized))
|
||||
{
|
||||
return "sin dato";
|
||||
}
|
||||
|
||||
const int maxLength = 80;
|
||||
return normalized.Length <= maxLength
|
||||
? normalized
|
||||
: normalized[..maxLength] + "...";
|
||||
}
|
||||
|
||||
private static ThirdPartyIdentityData NormalizeThirdParty(ThirdPartyIdentityData thirdParty)
|
||||
@@ -563,6 +1039,9 @@ namespace ApiDenuncias.Services
|
||||
BusinessName = businessName,
|
||||
Email = (thirdParty.Email ?? string.Empty).Trim(),
|
||||
CountryCode = string.IsNullOrWhiteSpace(thirdParty.CountryCode) ? "ESP" : thirdParty.CountryCode.Trim(),
|
||||
NotificationPreference = thirdParty.NotificationPreference ?? string.Empty,
|
||||
ElectronicNotification = thirdParty.ElectronicNotification ?? string.Empty,
|
||||
PostalNotificationPreference = thirdParty.PostalNotificationPreference ?? string.Empty,
|
||||
Address = thirdParty.Address
|
||||
};
|
||||
}
|
||||
@@ -577,6 +1056,9 @@ namespace ApiDenuncias.Services
|
||||
BusinessName = string.Empty,
|
||||
Email = string.Empty,
|
||||
CountryCode = string.IsNullOrWhiteSpace(thirdParty.CountryCode) ? "ESP" : thirdParty.CountryCode,
|
||||
NotificationPreference = thirdParty.NotificationPreference ?? string.Empty,
|
||||
ElectronicNotification = thirdParty.ElectronicNotification ?? string.Empty,
|
||||
PostalNotificationPreference = thirdParty.PostalNotificationPreference ?? string.Empty,
|
||||
Address = null
|
||||
};
|
||||
}
|
||||
@@ -718,6 +1200,51 @@ namespace ApiDenuncias.Services
|
||||
return null;
|
||||
}
|
||||
|
||||
public async Task<GestionaExpedienteInfo?> BuscarExpedientePorCodigoAsync(string codigoExpediente)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(codigoExpediente))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var normalizedCode = codigoExpediente.Trim();
|
||||
var json = await GetFilesAsync(new
|
||||
{
|
||||
code = normalizedCode
|
||||
});
|
||||
using var doc = JsonDocument.Parse(json);
|
||||
|
||||
if (!TryGetFilesContent(doc, out var content) || content.GetArrayLength() == 0)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
GestionaExpedienteInfo? first = null;
|
||||
foreach (var item in content.EnumerateArray())
|
||||
{
|
||||
var expediente = BuildExpedienteInfo(item);
|
||||
if (expediente is null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (first is null)
|
||||
{
|
||||
first = expediente;
|
||||
}
|
||||
|
||||
if (string.Equals(
|
||||
expediente.CodigoExpediente?.Trim(),
|
||||
normalizedCode,
|
||||
StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return expediente;
|
||||
}
|
||||
}
|
||||
|
||||
return first;
|
||||
}
|
||||
|
||||
public async Task<GestionaExpedienteInfo?> ObtenerExpedienteAsync(string fileUrl)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(fileUrl))
|
||||
@@ -797,13 +1324,78 @@ namespace ApiDenuncias.Services
|
||||
}
|
||||
|
||||
|
||||
public async Task<GestionaAuditInfo?> ObtenerUltimaAuditoriaExpedienteAsync(string fileUrl)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(fileUrl))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var auditUrl = $"{fileUrl.TrimEnd('/')}/audit";
|
||||
using var req = new HttpRequestMessage(HttpMethod.Get, auditUrl);
|
||||
req.Headers.TryAddWithoutValidation("X-Gestiona-Access-Token", _opts.AccessToken);
|
||||
req.Headers.Accept.Clear();
|
||||
req.Headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse("application/vnd.gestiona.audit.entries-page"));
|
||||
|
||||
using var resp = await _http.SendAsync(req);
|
||||
LogDeprecatedHeaders(resp, $"GET {auditUrl}");
|
||||
if (resp.StatusCode == System.Net.HttpStatusCode.NoContent ||
|
||||
resp.StatusCode == System.Net.HttpStatusCode.NotFound)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var body = await resp.Content.ReadAsStringAsync();
|
||||
if (!resp.IsSuccessStatusCode)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"ObtenerUltimaAuditoriaExpedienteAsync: {(int)resp.StatusCode} {resp.ReasonPhrase}\n{body}");
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(body))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
using var doc = JsonDocument.Parse(body);
|
||||
if (!doc.RootElement.TryGetProperty("content", out var content) ||
|
||||
content.ValueKind != JsonValueKind.Array ||
|
||||
content.GetArrayLength() == 0)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
GestionaAuditInfo? latest = null;
|
||||
foreach (var item in content.EnumerateArray())
|
||||
{
|
||||
var entryDate = GetJsonDateTimeOffset(item, "entry_date");
|
||||
if (entryDate is null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (latest?.Fecha is not null && entryDate.Value <= latest.Fecha.Value)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
latest = new GestionaAuditInfo
|
||||
{
|
||||
Fecha = entryDate,
|
||||
Mensaje = GetJsonString(item, "message")
|
||||
};
|
||||
}
|
||||
|
||||
return latest;
|
||||
}
|
||||
|
||||
public async Task<List<ExpedienteTerceroDto>> ObtenerExpedientesPorTerceroAsync(
|
||||
string nif,
|
||||
DateTimeOffset? desde = null,
|
||||
DateTimeOffset? hasta = null,
|
||||
int maxPages = 1,
|
||||
int maxResults = 30,
|
||||
int maxParallel = 6 // de momento NO se usa, dejamos la firma por compatibilidad
|
||||
int maxParallel = 6
|
||||
)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(nif))
|
||||
@@ -811,21 +1403,11 @@ namespace ApiDenuncias.Services
|
||||
|
||||
nif = nif.Trim().ToUpperInvariant();
|
||||
_ = maxPages;
|
||||
_ = maxParallel;
|
||||
|
||||
// 1) Localizar el tercero por NIF
|
||||
var tercero = await BuscarTerceroPorNifAsync(nif);
|
||||
if (string.IsNullOrEmpty(tercero.SelfHref))
|
||||
return new List<ExpedienteTerceroDto>(); // no hay tercero => no hay expedientes
|
||||
|
||||
var resultados = new List<ExpedienteTerceroDto>();
|
||||
var json = await GetFilesAsync(new
|
||||
{
|
||||
third_rest_link = new
|
||||
{
|
||||
rel = "third",
|
||||
href = tercero.SelfHref
|
||||
}
|
||||
dni = nif
|
||||
});
|
||||
using var doc = JsonDocument.Parse(json);
|
||||
|
||||
@@ -841,20 +1423,15 @@ namespace ApiDenuncias.Services
|
||||
break;
|
||||
}
|
||||
|
||||
DateTimeOffset? creation = null;
|
||||
if (item.TryGetProperty("creation_date", out var pCreation))
|
||||
{
|
||||
if (pCreation.ValueKind == JsonValueKind.Number &&
|
||||
pCreation.TryGetInt64(out var ts))
|
||||
{
|
||||
creation = DateTimeOffset.FromUnixTimeSeconds(ts);
|
||||
}
|
||||
else if (pCreation.ValueKind == JsonValueKind.String &&
|
||||
long.TryParse(pCreation.GetString(), out var tsString))
|
||||
{
|
||||
creation = DateTimeOffset.FromUnixTimeSeconds(tsString);
|
||||
}
|
||||
}
|
||||
var creation = GetJsonDateTimeOffset(item, "creation_date");
|
||||
var updated = GetFirstJsonDateTimeOffset(
|
||||
item,
|
||||
"update_date",
|
||||
"updated_at",
|
||||
"modification_date",
|
||||
"modified_at",
|
||||
"last_update",
|
||||
"last_modified");
|
||||
|
||||
if (desde.HasValue && creation.HasValue && creation.Value < desde.Value)
|
||||
continue;
|
||||
@@ -905,14 +1482,67 @@ namespace ApiDenuncias.Services
|
||||
FileUrl = fileUrl,
|
||||
CodigoExpediente = code,
|
||||
Asunto = asunto,
|
||||
Procedimiento = procedureName,
|
||||
FechaCreacion = creation,
|
||||
FechaUltimaModificacion = updated ?? creation,
|
||||
Estado = state
|
||||
});
|
||||
}
|
||||
|
||||
await EnrichExpedientesWithAuditAsync(resultados, maxParallel);
|
||||
return resultados;
|
||||
}
|
||||
|
||||
private async Task EnrichExpedientesWithAuditAsync(
|
||||
IReadOnlyList<ExpedienteTerceroDto> expedientes,
|
||||
int maxParallel)
|
||||
{
|
||||
if (expedientes.Count == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var parallelism = Math.Clamp(maxParallel, 1, 6);
|
||||
using var gate = new SemaphoreSlim(parallelism, parallelism);
|
||||
var tasks = expedientes
|
||||
.Where(expediente => !string.IsNullOrWhiteSpace(expediente.FileUrl))
|
||||
.Select(async expediente =>
|
||||
{
|
||||
await gate.WaitAsync();
|
||||
try
|
||||
{
|
||||
var audit = await ObtenerUltimaAuditoriaExpedienteAsync(expediente.FileUrl);
|
||||
if (audit is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (audit.Fecha is not null)
|
||||
{
|
||||
expediente.FechaUltimaModificacion = audit.Fecha;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(audit.Mensaje))
|
||||
{
|
||||
expediente.UltimaAuditoriaMensaje = audit.Mensaje;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogWarning(
|
||||
ex,
|
||||
"No se ha podido consultar la auditoria del expediente {FileUrl}.",
|
||||
expediente.FileUrl);
|
||||
}
|
||||
finally
|
||||
{
|
||||
gate.Release();
|
||||
}
|
||||
});
|
||||
|
||||
await Task.WhenAll(tasks);
|
||||
}
|
||||
|
||||
private async Task TryEnsureThirdAddressAsync(string thirdSelfHref, ThirdPartyAddressData address)
|
||||
{
|
||||
if (!address.HasAnyValue)
|
||||
@@ -1138,6 +1768,58 @@ namespace ApiDenuncias.Services
|
||||
: null;
|
||||
}
|
||||
|
||||
private static DateTimeOffset? GetFirstJsonDateTimeOffset(JsonElement item, params string[] propertyNames)
|
||||
{
|
||||
foreach (var propertyName in propertyNames)
|
||||
{
|
||||
var value = GetJsonDateTimeOffset(item, propertyName);
|
||||
if (value is not null)
|
||||
{
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private static DateTimeOffset? GetJsonDateTimeOffset(JsonElement item, string propertyName)
|
||||
{
|
||||
if (!item.TryGetProperty(propertyName, out var property))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
if (property.ValueKind == JsonValueKind.Number &&
|
||||
property.TryGetInt64(out var timestamp))
|
||||
{
|
||||
return DateTimeOffset.FromUnixTimeSeconds(timestamp);
|
||||
}
|
||||
|
||||
if (property.ValueKind != JsonValueKind.String)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var raw = property.GetString();
|
||||
if (string.IsNullOrWhiteSpace(raw))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
if (long.TryParse(raw, NumberStyles.Integer, CultureInfo.InvariantCulture, out var timestampString))
|
||||
{
|
||||
return DateTimeOffset.FromUnixTimeSeconds(timestampString);
|
||||
}
|
||||
|
||||
return DateTimeOffset.TryParse(
|
||||
raw.Replace("Z", "+00:00", StringComparison.Ordinal),
|
||||
CultureInfo.InvariantCulture,
|
||||
DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal,
|
||||
out var parsed)
|
||||
? parsed
|
||||
: null;
|
||||
}
|
||||
|
||||
private static string? FirstNonEmpty(params string?[] values)
|
||||
{
|
||||
foreach (var value in values)
|
||||
@@ -1171,7 +1853,25 @@ namespace ApiDenuncias.Services
|
||||
|
||||
private static string BuildNotificationChannel(ThirdPartyIdentityData thirdParty)
|
||||
{
|
||||
_ = thirdParty;
|
||||
var notificationText = NormalizeKey(string.Join(
|
||||
' ',
|
||||
thirdParty.NotificationPreference,
|
||||
thirdParty.ElectronicNotification,
|
||||
thirdParty.PostalNotificationPreference));
|
||||
|
||||
var requestsPostal =
|
||||
notificationText.Contains("CORREO POSTAL", StringComparison.Ordinal) ||
|
||||
notificationText.Contains("POSTAL", StringComparison.Ordinal);
|
||||
var requestsElectronic =
|
||||
notificationText.Contains("ELECTRONICA", StringComparison.Ordinal) ||
|
||||
notificationText.Contains("TELEMATICA", StringComparison.Ordinal) ||
|
||||
notificationText.Contains("ONLINE", StringComparison.Ordinal);
|
||||
|
||||
if (requestsPostal && thirdParty.Address?.HasAnyValue == true && !requestsElectronic)
|
||||
{
|
||||
return "PAPER";
|
||||
}
|
||||
|
||||
return "TELEMATIC";
|
||||
}
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,83 @@
|
||||
using System.Collections.Concurrent;
|
||||
using GestionaDenuncias.Shared.Models;
|
||||
|
||||
namespace ApiDenuncias.Services;
|
||||
|
||||
public sealed class GlobalLeaksSessionKeepAliveService
|
||||
{
|
||||
private static readonly TimeSpan MinimumRefreshInterval = TimeSpan.FromSeconds(20);
|
||||
private static readonly ConcurrentDictionary<string, SemaphoreSlim> UserGates =
|
||||
new(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
private readonly GlobalLeaksSessionStore _sessionStore;
|
||||
private readonly GlobalLeaksClient _globalLeaksClient;
|
||||
private readonly ILogger<GlobalLeaksSessionKeepAliveService> _logger;
|
||||
|
||||
public GlobalLeaksSessionKeepAliveService(
|
||||
GlobalLeaksSessionStore sessionStore,
|
||||
GlobalLeaksClient globalLeaksClient,
|
||||
ILogger<GlobalLeaksSessionKeepAliveService> logger)
|
||||
{
|
||||
_sessionStore = sessionStore;
|
||||
_globalLeaksClient = globalLeaksClient;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public async Task<GlobalLeaksStoredSession?> KeepAliveAsync(
|
||||
string username,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var gate = UserGates.GetOrAdd(username, static _ => new SemaphoreSlim(1, 1));
|
||||
await gate.WaitAsync(cancellationToken);
|
||||
try
|
||||
{
|
||||
var session = await _sessionStore.GetAsync(username, cancellationToken);
|
||||
if (session?.HasActiveSession != true)
|
||||
{
|
||||
return session;
|
||||
}
|
||||
|
||||
if (session.LastKeepAliveAtUtc is { } lastKeepAlive &&
|
||||
DateTimeOffset.UtcNow - lastKeepAlive < MinimumRefreshInterval)
|
||||
{
|
||||
return session;
|
||||
}
|
||||
|
||||
var expectedSessionId = session.SessionId!;
|
||||
try
|
||||
{
|
||||
var refreshed = await _globalLeaksClient.RefreshSessionAsync(
|
||||
expectedSessionId,
|
||||
session.DpopPrivateKey!,
|
||||
session.ProofOfWorkToken!,
|
||||
cancellationToken);
|
||||
|
||||
var updated = await _sessionStore.UpdateKeepAliveAsync(
|
||||
username,
|
||||
expectedSessionId,
|
||||
refreshed.ProofOfWorkToken,
|
||||
refreshed.SessionExpiresAtUtc,
|
||||
cancellationToken);
|
||||
|
||||
if (updated)
|
||||
{
|
||||
_logger.LogDebug("Sesion GlobalLeaks renovada para {Username}.", username);
|
||||
}
|
||||
|
||||
return await _sessionStore.GetAsync(username, cancellationToken);
|
||||
}
|
||||
catch (GlobalLeaksSessionExpiredException)
|
||||
{
|
||||
await _sessionStore.ClearSessionIfMatchesAsync(
|
||||
username,
|
||||
expectedSessionId,
|
||||
cancellationToken);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
gate.Release();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -8,7 +8,7 @@ namespace ApiDenuncias.Services;
|
||||
|
||||
public sealed class GlobalLeaksSessionStore
|
||||
{
|
||||
private const string RootPath = @"C:\ZipsDenuncias\.gl-auth";
|
||||
private const string RootPath = @"C:\GestionaDenuncias\.gl-auth";
|
||||
private static readonly JsonSerializerOptions JsonOptions = new(JsonSerializerDefaults.Web)
|
||||
{
|
||||
WriteIndented = false,
|
||||
@@ -30,18 +30,10 @@ public sealed class GlobalLeaksSessionStore
|
||||
}
|
||||
|
||||
var path = GetFilePath(username);
|
||||
if (!File.Exists(path))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
await _gate.WaitAsync(cancellationToken);
|
||||
try
|
||||
{
|
||||
var protectedBytes = await File.ReadAllBytesAsync(path, cancellationToken);
|
||||
var protectedBase64 = Encoding.UTF8.GetString(protectedBytes);
|
||||
var json = _protector.Unprotect(protectedBase64);
|
||||
return JsonSerializer.Deserialize<GlobalLeaksStoredSession>(json, JsonOptions);
|
||||
return await ReadUnsafeAsync(path, cancellationToken);
|
||||
}
|
||||
finally
|
||||
{
|
||||
@@ -54,15 +46,23 @@ public sealed class GlobalLeaksSessionStore
|
||||
string password,
|
||||
string sessionId,
|
||||
string? role,
|
||||
string? dpopPrivateKey,
|
||||
GlobalLeaksProofOfWorkToken? proofOfWorkToken,
|
||||
DateTimeOffset? sessionExpiresAtUtc,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var now = DateTimeOffset.UtcNow;
|
||||
var data = new GlobalLeaksStoredSession
|
||||
{
|
||||
Username = username,
|
||||
Password = password,
|
||||
SessionId = sessionId,
|
||||
Role = role,
|
||||
UpdatedAt = DateTimeOffset.UtcNow,
|
||||
DpopPrivateKey = dpopPrivateKey,
|
||||
ProofOfWorkToken = proofOfWorkToken,
|
||||
SessionExpiresAtUtc = sessionExpiresAtUtc,
|
||||
LastKeepAliveAtUtc = now,
|
||||
UpdatedAt = now,
|
||||
};
|
||||
|
||||
await WriteAsync(data, cancellationToken);
|
||||
@@ -72,29 +72,116 @@ public sealed class GlobalLeaksSessionStore
|
||||
string username,
|
||||
string sessionId,
|
||||
string? role,
|
||||
string? dpopPrivateKey,
|
||||
GlobalLeaksProofOfWorkToken? proofOfWorkToken,
|
||||
DateTimeOffset? sessionExpiresAtUtc,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var current = await GetAsync(username, cancellationToken)
|
||||
var path = GetFilePath(username);
|
||||
|
||||
await _gate.WaitAsync(cancellationToken);
|
||||
try
|
||||
{
|
||||
var current = await ReadUnsafeAsync(path, cancellationToken)
|
||||
?? throw new InvalidOperationException("No hay credenciales guardadas para este usuario.");
|
||||
var now = DateTimeOffset.UtcNow;
|
||||
|
||||
current.SessionId = sessionId;
|
||||
current.Role = role;
|
||||
current.UpdatedAt = DateTimeOffset.UtcNow;
|
||||
current.DpopPrivateKey = dpopPrivateKey;
|
||||
current.ProofOfWorkToken = proofOfWorkToken;
|
||||
current.SessionExpiresAtUtc = sessionExpiresAtUtc;
|
||||
current.LastKeepAliveAtUtc = now;
|
||||
current.UpdatedAt = now;
|
||||
|
||||
await WriteAsync(current, cancellationToken);
|
||||
await WriteUnsafeAsync(path, current, cancellationToken);
|
||||
}
|
||||
finally
|
||||
{
|
||||
_gate.Release();
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<bool> UpdateKeepAliveAsync(
|
||||
string username,
|
||||
string expectedSessionId,
|
||||
GlobalLeaksProofOfWorkToken proofOfWorkToken,
|
||||
DateTimeOffset? sessionExpiresAtUtc,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var path = GetFilePath(username);
|
||||
|
||||
await _gate.WaitAsync(cancellationToken);
|
||||
try
|
||||
{
|
||||
var current = await ReadUnsafeAsync(path, cancellationToken);
|
||||
if (current is null ||
|
||||
!string.Equals(current.SessionId, expectedSessionId, StringComparison.Ordinal))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var now = DateTimeOffset.UtcNow;
|
||||
current.ProofOfWorkToken = proofOfWorkToken;
|
||||
current.SessionExpiresAtUtc = sessionExpiresAtUtc;
|
||||
current.LastKeepAliveAtUtc = now;
|
||||
current.UpdatedAt = now;
|
||||
await WriteUnsafeAsync(path, current, cancellationToken);
|
||||
return true;
|
||||
}
|
||||
finally
|
||||
{
|
||||
_gate.Release();
|
||||
}
|
||||
}
|
||||
|
||||
public async Task ClearSessionAsync(string username, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var current = await GetAsync(username, cancellationToken);
|
||||
var path = GetFilePath(username);
|
||||
|
||||
await _gate.WaitAsync(cancellationToken);
|
||||
try
|
||||
{
|
||||
var current = await ReadUnsafeAsync(path, cancellationToken);
|
||||
if (current is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
current.SessionId = null;
|
||||
current.UpdatedAt = DateTimeOffset.UtcNow;
|
||||
await WriteAsync(current, cancellationToken);
|
||||
ClearSessionValues(current);
|
||||
await WriteUnsafeAsync(path, current, cancellationToken);
|
||||
}
|
||||
finally
|
||||
{
|
||||
_gate.Release();
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<bool> ClearSessionIfMatchesAsync(
|
||||
string username,
|
||||
string expectedSessionId,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var path = GetFilePath(username);
|
||||
|
||||
await _gate.WaitAsync(cancellationToken);
|
||||
try
|
||||
{
|
||||
var current = await ReadUnsafeAsync(path, cancellationToken);
|
||||
if (current is null ||
|
||||
!string.Equals(current.SessionId, expectedSessionId, StringComparison.Ordinal))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
ClearSessionValues(current);
|
||||
await WriteUnsafeAsync(path, current, cancellationToken);
|
||||
return true;
|
||||
}
|
||||
finally
|
||||
{
|
||||
_gate.Release();
|
||||
}
|
||||
}
|
||||
|
||||
public async Task DeleteAsync(string username, CancellationToken cancellationToken = default)
|
||||
@@ -122,17 +209,12 @@ public sealed class GlobalLeaksSessionStore
|
||||
|
||||
private async Task WriteAsync(GlobalLeaksStoredSession data, CancellationToken cancellationToken)
|
||||
{
|
||||
Directory.CreateDirectory(RootPath);
|
||||
|
||||
var path = GetFilePath(data.Username);
|
||||
var json = JsonSerializer.Serialize(data, JsonOptions);
|
||||
var protectedValue = _protector.Protect(json);
|
||||
var protectedBytes = Encoding.UTF8.GetBytes(protectedValue);
|
||||
|
||||
await _gate.WaitAsync(cancellationToken);
|
||||
try
|
||||
{
|
||||
await File.WriteAllBytesAsync(path, protectedBytes, cancellationToken);
|
||||
await WriteUnsafeAsync(path, data, cancellationToken);
|
||||
}
|
||||
finally
|
||||
{
|
||||
@@ -147,4 +229,41 @@ public sealed class GlobalLeaksSessionStore
|
||||
var hash = SHA256.HashData(Encoding.UTF8.GetBytes(normalized));
|
||||
return Path.Combine(RootPath, $"{Convert.ToHexString(hash).ToLowerInvariant()}.bin");
|
||||
}
|
||||
|
||||
private async Task<GlobalLeaksStoredSession?> ReadUnsafeAsync(
|
||||
string path,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
if (!File.Exists(path))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var protectedBytes = await File.ReadAllBytesAsync(path, cancellationToken);
|
||||
var protectedBase64 = Encoding.UTF8.GetString(protectedBytes);
|
||||
var json = _protector.Unprotect(protectedBase64);
|
||||
return JsonSerializer.Deserialize<GlobalLeaksStoredSession>(json, JsonOptions);
|
||||
}
|
||||
|
||||
private async Task WriteUnsafeAsync(
|
||||
string path,
|
||||
GlobalLeaksStoredSession data,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
Directory.CreateDirectory(RootPath);
|
||||
var json = JsonSerializer.Serialize(data, JsonOptions);
|
||||
var protectedValue = _protector.Protect(json);
|
||||
var protectedBytes = Encoding.UTF8.GetBytes(protectedValue);
|
||||
await File.WriteAllBytesAsync(path, protectedBytes, cancellationToken);
|
||||
}
|
||||
|
||||
private static void ClearSessionValues(GlobalLeaksStoredSession session)
|
||||
{
|
||||
session.SessionId = null;
|
||||
session.DpopPrivateKey = null;
|
||||
session.ProofOfWorkToken = null;
|
||||
session.SessionExpiresAtUtc = null;
|
||||
session.LastKeepAliveAtUtc = null;
|
||||
session.UpdatedAt = DateTimeOffset.UtcNow;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,10 @@ namespace ApiDenuncias.Services;
|
||||
|
||||
public interface IFilteredDenunciaStore
|
||||
{
|
||||
Task<DenunciasGestiona?> GetDenunciaByGestionaFileCodeAsync(
|
||||
string gestionaFileCode,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
Task<List<DenunciasGestiona>> GetDenunciasByIdsAsync(
|
||||
IReadOnlyCollection<int> denunciaIds,
|
||||
CancellationToken cancellationToken = default);
|
||||
@@ -16,4 +20,8 @@ public interface IFilteredDenunciaStore
|
||||
Task<List<FicherosDenuncias>> GetFicherosByDenunciaIdsAsync(
|
||||
IReadOnlyCollection<int> denunciaIds,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
Task<List<FicherosDenuncias>> GetFicherosMetadataByDenunciaAsync(
|
||||
int denunciaId,
|
||||
CancellationToken cancellationToken = default);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using GestionaDenuncias.Shared.Models;
|
||||
using GestionaDenuncias.Shared.Models;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
@@ -12,26 +12,31 @@ namespace ApiDenuncias.Services
|
||||
// =========================
|
||||
|
||||
/// <summary>
|
||||
/// Crea un expediente para el procedimiento indicado y devuelve los links 'file' y 'file-open'.
|
||||
/// Crea un expediente resolviendo el procedimiento externo desde el catalogo de Gestiona y devuelve los links 'file' y 'file-open'.
|
||||
/// </summary>
|
||||
Task<GestionaCreateFileResponse> CreateFileAsync(
|
||||
Guid procedureId,
|
||||
string subject,
|
||||
string documentSeries,
|
||||
string siaCode
|
||||
);
|
||||
|
||||
/// <summary>
|
||||
/// Abre el expediente (lo pone en OPEN_EDITABLE), asigna t<EFBFBD>tulo, clasificaci<EFBFBD>n y lo vincula al grupo indicado.
|
||||
/// Abre el expediente (lo pone en OPEN_EDITABLE), asigna título, clasificación y lo vincula al grupo indicado.
|
||||
/// </summary>
|
||||
Task OpenFileAsync(
|
||||
string fileUrl,
|
||||
string? fileOpenUrl,
|
||||
Guid managementUnitGroupId,
|
||||
Guid assignedGroupId,
|
||||
string assignedGroupCode,
|
||||
bool confidential,
|
||||
string freeTitle,
|
||||
string siaCode
|
||||
string freeTitle
|
||||
);
|
||||
|
||||
/// <summary>
|
||||
/// Reemplaza los grupos/usuarios asignados al expediente. No modifica la unidad gestora.
|
||||
/// </summary>
|
||||
Task AssignFileAsync(
|
||||
string fileUrl,
|
||||
string assignedGroupCode
|
||||
);
|
||||
|
||||
// =========================
|
||||
@@ -47,7 +52,7 @@ namespace ApiDenuncias.Services
|
||||
);
|
||||
|
||||
/// <summary>
|
||||
/// Crea el documento (metadata) y sube el contenido PDF a la ra<EFBFBD>z o a una carpeta.
|
||||
/// Crea el documento (metadata) y sube el contenido PDF a la raíz o a una carpeta.
|
||||
/// </summary>
|
||||
Task UploadDocumentAsync(
|
||||
string fileUrl,
|
||||
@@ -56,7 +61,7 @@ namespace ApiDenuncias.Services
|
||||
);
|
||||
|
||||
/// <summary>
|
||||
/// Crea una carpeta de nombre 'folderName' en la ra<EFBFBD>z del expediente y devuelve su GUID.
|
||||
/// Crea una carpeta de nombre 'folderName' en la raíz del expediente y devuelve su GUID.
|
||||
/// </summary>
|
||||
Task<Guid> CreateFolderAsync(
|
||||
string fileUrl,
|
||||
@@ -89,11 +94,11 @@ namespace ApiDenuncias.Services
|
||||
|
||||
/// <summary>
|
||||
/// Usa el NIF tal cual viene.
|
||||
/// Si es an<EFBFBD>nimo o vac<EFBFBD>o ? no crea ni enlaza.
|
||||
/// Si es anónimo o vacío ? no crea ni enlaza.
|
||||
/// Si no existe, lo crea.
|
||||
/// Si no est<EFBFBD> enlazado al expediente, lo enlaza.
|
||||
/// Si no está enlazado al expediente, lo enlaza.
|
||||
/// </summary>
|
||||
Task AsegurarTerceroYEnlazarAsync(string fileUrl, ThirdPartyIdentityData thirdParty);
|
||||
Task<GestionaEnsureThirdResponse> AsegurarTerceroYEnlazarAsync(string fileUrl, ThirdPartyIdentityData thirdParty);
|
||||
|
||||
|
||||
|
||||
@@ -102,21 +107,31 @@ namespace ApiDenuncias.Services
|
||||
// =========================
|
||||
|
||||
/// <summary>
|
||||
/// Devuelve el JSON crudo del listado operativo de expedientes, sin recorrer hist<EFBFBD>rico paginado.
|
||||
/// Devuelve el JSON crudo del listado operativo de expedientes, sin recorrer histórico paginado.
|
||||
/// </summary>
|
||||
Task<string> ListarExpedientesJsonAsyncBasico(int maxPages = 1);
|
||||
|
||||
/// <summary>
|
||||
/// Busca directamente un expediente cuyo asunto sea "Denuncia {idDenuncia}-CD".
|
||||
/// Devuelve URL, n<EFBFBD>mero de expediente y t<EFBFBD>tulo si lo encuentra; null si no.
|
||||
/// Devuelve URL, número de expediente y título si lo encuentra; null si no.
|
||||
/// </summary>
|
||||
Task<GestionaExpedienteInfo?> BuscarExpedientePorIdEnAsuntoAsync(int idDenuncia);
|
||||
|
||||
/// <summary>
|
||||
/// Busca un expediente por su numero/codigo visible de Gestiona, por ejemplo "151/2025".
|
||||
/// </summary>
|
||||
Task<GestionaExpedienteInfo?> BuscarExpedientePorCodigoAsync(string codigoExpediente);
|
||||
|
||||
/// <summary>
|
||||
/// Obtiene los metadatos visibles de un expediente concreto.
|
||||
/// </summary>
|
||||
Task<GestionaExpedienteInfo?> ObtenerExpedienteAsync(string fileUrl);
|
||||
|
||||
/// <summary>
|
||||
/// Obtiene la última entrada de auditoría del expediente, si Gestiona devuelve contenido.
|
||||
/// </summary>
|
||||
Task<GestionaAuditInfo?> ObtenerUltimaAuditoriaExpedienteAsync(string fileUrl);
|
||||
|
||||
Task<List<ExpedienteTerceroDto>> ObtenerExpedientesPorTerceroAsync(
|
||||
string nif,
|
||||
DateTimeOffset? desde = null,
|
||||
@@ -130,3 +145,5 @@ namespace ApiDenuncias.Services
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -102,18 +102,28 @@ public sealed class InboxTrackingService : IInboxTrackingService
|
||||
DownloadedByAnotherUser = meta?.DownloadedByAnotherUser ?? false,
|
||||
LastDownloadedByUsername = meta?.LastDownloadedByUsername,
|
||||
LastDownloadedAt = meta?.LastDownloadedAtUtc?.ToString("O", CultureInfo.InvariantCulture),
|
||||
LastGestionaUploadAt = meta?.LastGestionaUploadAtUtc?.ToString("O", CultureInfo.InvariantCulture),
|
||||
AlreadyImported = meta?.AlreadyImported ?? false,
|
||||
AlreadyInGestiona = meta?.AlreadyInGestiona ?? false,
|
||||
OwnerUsername = meta?.OwnerUsername,
|
||||
OwnedByCurrentUser = meta?.OwnedByCurrentUser ?? false,
|
||||
AccessibleByWorkGroup = meta?.AccessibleByWorkGroup ?? false,
|
||||
RequiresOwnerConfirmation = meta?.RequiresOwnerConfirmation ?? false,
|
||||
OwnerWorkGroups = meta?.OwnerWorkGroups ?? [],
|
||||
TrackingNote = BuildTrackingNote(meta)
|
||||
};
|
||||
})
|
||||
.Where(report => !IsLockedByAnotherUser(report))
|
||||
.Where(report =>
|
||||
string.IsNullOrWhiteSpace(report.OwnerUsername) ||
|
||||
report.OwnedByCurrentUser ||
|
||||
report.AccessibleByWorkGroup)
|
||||
.ToArray();
|
||||
}
|
||||
|
||||
public async Task EnsureReportCanBeImportedByUserAsync(
|
||||
string username,
|
||||
ReportDto report,
|
||||
bool confirmDifferentOwner = false,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
await _denunciaStore.EnsureSchemaAsync(cancellationToken);
|
||||
@@ -140,14 +150,23 @@ public sealed class InboxTrackingService : IInboxTrackingService
|
||||
|
||||
await EnsureConnectionOpenAsync(connection, cancellationToken);
|
||||
var metadata = await LoadMetadataAsync(connection, userId, [report.Id], cancellationToken);
|
||||
if (metadata.TryGetValue(report.Id, out var meta) && meta.LockedByAnotherUser)
|
||||
if (!metadata.TryGetValue(report.Id, out var meta) ||
|
||||
string.IsNullOrWhiteSpace(meta.OwnerUsername) ||
|
||||
meta.OwnedByCurrentUser)
|
||||
{
|
||||
var owner = string.IsNullOrWhiteSpace(meta.LastDownloadedByUsername)
|
||||
? "otro usuario"
|
||||
: meta.LastDownloadedByUsername;
|
||||
return;
|
||||
}
|
||||
|
||||
throw new InvalidOperationException(
|
||||
$"La denuncia ya fue importada por {owner}. Solo ese usuario puede ver e importar sus actualizaciones.");
|
||||
if (!meta.AccessibleByWorkGroup)
|
||||
{
|
||||
throw new ReportOwnershipException(
|
||||
$"La denuncia pertenece a {meta.OwnerUsername} y no compartis ningun grupo de trabajo.");
|
||||
}
|
||||
|
||||
if (!confirmDifferentOwner)
|
||||
{
|
||||
throw new ReportOwnershipException(
|
||||
$"La denuncia pertenece a {meta.OwnerUsername}. Confirma expresamente que deseas importarla como miembro de su grupo.");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -180,6 +199,7 @@ public sealed class InboxTrackingService : IInboxTrackingService
|
||||
SET
|
||||
last_downloaded_at_utc = @nowUtc,
|
||||
last_downloaded_by_user_id = @userId,
|
||||
owner_user_id = COALESCE(owner_user_id, @userId),
|
||||
imported_complaint_report_id = COALESCE(@complaintId, imported_complaint_report_id),
|
||||
imported_to_store_at_utc = COALESCE(imported_to_store_at_utc, @nowUtc),
|
||||
updated_at_utc = CURRENT_TIMESTAMP(6)
|
||||
@@ -262,6 +282,107 @@ public sealed class InboxTrackingService : IInboxTrackingService
|
||||
}
|
||||
}
|
||||
|
||||
public async Task MarkReportHandledInGestionaAsync(
|
||||
string username,
|
||||
int denunciaId,
|
||||
DateTime uploadedAtUtc,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
await _denunciaStore.EnsureSchemaAsync(cancellationToken);
|
||||
|
||||
if (string.IsNullOrWhiteSpace(username) || denunciaId <= 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var handledAtUtc = uploadedAtUtc.Kind switch
|
||||
{
|
||||
DateTimeKind.Utc => uploadedAtUtc,
|
||||
DateTimeKind.Local => uploadedAtUtc.ToUniversalTime(),
|
||||
_ => DateTime.SpecifyKind(uploadedAtUtc, DateTimeKind.Utc)
|
||||
};
|
||||
|
||||
await using var connection = await OpenConnectionAsync(cancellationToken);
|
||||
var userId = await EnsureUserAsync(connection, username, cancellationToken);
|
||||
await using var transaction = await connection.BeginTransactionAsync(cancellationToken);
|
||||
|
||||
try
|
||||
{
|
||||
const string updateInboxSql = """
|
||||
UPDATE inbox_reports
|
||||
SET
|
||||
last_downloaded_at_utc =
|
||||
CASE
|
||||
WHEN last_downloaded_at_utc IS NULL THEN @handledAtUtc
|
||||
WHEN @handledAtUtc > last_downloaded_at_utc THEN @handledAtUtc
|
||||
ELSE last_downloaded_at_utc
|
||||
END,
|
||||
last_downloaded_by_user_id = @userId,
|
||||
owner_user_id = COALESCE(owner_user_id, @userId),
|
||||
imported_complaint_report_id = COALESCE(imported_complaint_report_id, @denunciaId),
|
||||
imported_to_store_at_utc = COALESCE(imported_to_store_at_utc, @handledAtUtc),
|
||||
updated_at_utc = CURRENT_TIMESTAMP(6)
|
||||
WHERE progressive_id = @denunciaId
|
||||
OR imported_complaint_report_id = @denunciaId;
|
||||
""";
|
||||
|
||||
await using (var updateInbox = new MySqlCommand(updateInboxSql, connection, (MySqlTransaction)transaction))
|
||||
{
|
||||
updateInbox.Parameters.AddWithValue("@handledAtUtc", handledAtUtc);
|
||||
updateInbox.Parameters.AddWithValue("@userId", userId);
|
||||
updateInbox.Parameters.AddWithValue("@denunciaId", denunciaId);
|
||||
await updateInbox.ExecuteNonQueryAsync(cancellationToken);
|
||||
}
|
||||
|
||||
const string updateUserReportSql = """
|
||||
INSERT INTO user_inbox_reports (
|
||||
app_user_id,
|
||||
inbox_report_id,
|
||||
first_seen_at_utc,
|
||||
last_seen_at_utc,
|
||||
first_downloaded_at_utc,
|
||||
last_downloaded_at_utc,
|
||||
download_count
|
||||
)
|
||||
SELECT
|
||||
@userId,
|
||||
ir.id,
|
||||
CURRENT_TIMESTAMP(6),
|
||||
CURRENT_TIMESTAMP(6),
|
||||
@handledAtUtc,
|
||||
@handledAtUtc,
|
||||
1
|
||||
FROM inbox_reports ir
|
||||
WHERE ir.progressive_id = @denunciaId
|
||||
OR ir.imported_complaint_report_id = @denunciaId
|
||||
ON DUPLICATE KEY UPDATE
|
||||
last_seen_at_utc = CURRENT_TIMESTAMP(6),
|
||||
first_downloaded_at_utc = COALESCE(first_downloaded_at_utc, VALUES(first_downloaded_at_utc)),
|
||||
last_downloaded_at_utc =
|
||||
CASE
|
||||
WHEN last_downloaded_at_utc IS NULL THEN VALUES(last_downloaded_at_utc)
|
||||
WHEN VALUES(last_downloaded_at_utc) > last_downloaded_at_utc THEN VALUES(last_downloaded_at_utc)
|
||||
ELSE last_downloaded_at_utc
|
||||
END;
|
||||
""";
|
||||
|
||||
await using (var updateUserReport = new MySqlCommand(updateUserReportSql, connection, (MySqlTransaction)transaction))
|
||||
{
|
||||
updateUserReport.Parameters.AddWithValue("@userId", userId);
|
||||
updateUserReport.Parameters.AddWithValue("@handledAtUtc", handledAtUtc);
|
||||
updateUserReport.Parameters.AddWithValue("@denunciaId", denunciaId);
|
||||
await updateUserReport.ExecuteNonQueryAsync(cancellationToken);
|
||||
}
|
||||
|
||||
await transaction.CommitAsync(cancellationToken);
|
||||
}
|
||||
catch
|
||||
{
|
||||
await SafeRollbackAsync(transaction, cancellationToken);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<long> EnsureUserAsync(MySqlConnection connection, string username, CancellationToken cancellationToken)
|
||||
{
|
||||
const string insertSql = """
|
||||
@@ -412,11 +533,45 @@ public sealed class InboxTrackingService : IInboxTrackingService
|
||||
ir.global_report_uuid,
|
||||
ir.last_downloaded_at_utc,
|
||||
downloader.username AS last_downloaded_by_username,
|
||||
owner.username AS owner_username,
|
||||
ir.imported_to_store_at_utc,
|
||||
COALESCE(c.is_in_gestiona, 0) AS already_in_gestiona,
|
||||
CASE WHEN uir.last_downloaded_at_utc IS NULL THEN 0 ELSE 1 END AS downloaded_by_current_user
|
||||
COALESCE(
|
||||
(
|
||||
SELECT MAX(history.uploaded_at_utc)
|
||||
FROM gestiona_upload_history history
|
||||
WHERE history.external_report_id =
|
||||
COALESCE(ir.imported_complaint_report_id, ir.progressive_id)
|
||||
),
|
||||
c.gestiona_uploaded_at_utc
|
||||
) AS last_gestiona_upload_at_utc,
|
||||
CASE WHEN uir.last_downloaded_at_utc IS NULL THEN 0 ELSE 1 END AS downloaded_by_current_user,
|
||||
CASE WHEN ir.owner_user_id = @userId THEN 1 ELSE 0 END AS owned_by_current_user,
|
||||
CASE
|
||||
WHEN ir.owner_user_id IS NULL THEN 0
|
||||
WHEN EXISTS (
|
||||
SELECT 1
|
||||
FROM app_user_groups shared_membership
|
||||
INNER JOIN work_groups active_group
|
||||
ON active_group.id = shared_membership.work_group_id
|
||||
AND active_group.is_active = 1
|
||||
WHERE shared_membership.app_user_id IN (@userId, ir.owner_user_id)
|
||||
GROUP BY shared_membership.work_group_id
|
||||
HAVING COUNT(DISTINCT shared_membership.app_user_id) = 2
|
||||
) THEN 1
|
||||
ELSE 0
|
||||
END AS accessible_by_group,
|
||||
(
|
||||
SELECT GROUP_CONCAT(DISTINCT wg.code ORDER BY wg.code SEPARATOR ',')
|
||||
FROM app_user_groups owner_membership
|
||||
INNER JOIN work_groups wg
|
||||
ON wg.id = owner_membership.work_group_id
|
||||
AND wg.is_active = 1
|
||||
WHERE owner_membership.app_user_id = ir.owner_user_id
|
||||
) AS owner_group_codes
|
||||
FROM inbox_reports ir
|
||||
LEFT JOIN app_users downloader ON downloader.id = ir.last_downloaded_by_user_id
|
||||
LEFT JOIN app_users owner ON owner.id = ir.owner_user_id
|
||||
LEFT JOIN user_inbox_reports uir
|
||||
ON uir.inbox_report_id = ir.id
|
||||
AND uir.app_user_id = @userId
|
||||
@@ -433,14 +588,20 @@ public sealed class InboxTrackingService : IInboxTrackingService
|
||||
? null
|
||||
: reader.GetString(reader.GetOrdinal("last_downloaded_by_username"));
|
||||
var downloadedByCurrentUser = reader.GetInt32(reader.GetOrdinal("downloaded_by_current_user")) == 1;
|
||||
var lockedByAnotherUser =
|
||||
!downloadedByCurrentUser &&
|
||||
!reader.IsDBNull(reader.GetOrdinal("imported_to_store_at_utc")) &&
|
||||
!string.IsNullOrWhiteSpace(lastDownloadedByUsername);
|
||||
|
||||
var downloadedByAnotherUser =
|
||||
!downloadedByCurrentUser &&
|
||||
!string.IsNullOrWhiteSpace(lastDownloadedByUsername);
|
||||
var ownerUsername = reader.IsDBNull(reader.GetOrdinal("owner_username"))
|
||||
? null
|
||||
: reader.GetString(reader.GetOrdinal("owner_username"));
|
||||
var ownedByCurrentUser =
|
||||
reader.GetInt32(reader.GetOrdinal("owned_by_current_user")) == 1;
|
||||
var accessibleByGroup =
|
||||
reader.GetInt32(reader.GetOrdinal("accessible_by_group")) == 1;
|
||||
var ownerWorkGroups = reader.IsDBNull(reader.GetOrdinal("owner_group_codes"))
|
||||
? []
|
||||
: reader.GetString(reader.GetOrdinal("owner_group_codes"))
|
||||
.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
|
||||
|
||||
metadata[reportId] = new ReportMetadata
|
||||
{
|
||||
@@ -448,9 +609,13 @@ public sealed class InboxTrackingService : IInboxTrackingService
|
||||
DownloadedByAnotherUser = downloadedByAnotherUser,
|
||||
LastDownloadedByUsername = lastDownloadedByUsername,
|
||||
LastDownloadedAtUtc = GetDateTimeOffset(reader, "last_downloaded_at_utc"),
|
||||
LastGestionaUploadAtUtc = GetDateTimeOffset(reader, "last_gestiona_upload_at_utc"),
|
||||
AlreadyImported = !reader.IsDBNull(reader.GetOrdinal("imported_to_store_at_utc")),
|
||||
AlreadyInGestiona = reader.GetInt32(reader.GetOrdinal("already_in_gestiona")) == 1,
|
||||
LockedByAnotherUser = lockedByAnotherUser,
|
||||
OwnerUsername = ownerUsername,
|
||||
OwnedByCurrentUser = ownedByCurrentUser,
|
||||
AccessibleByWorkGroup = accessibleByGroup,
|
||||
OwnerWorkGroups = ownerWorkGroups,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -554,11 +719,10 @@ public sealed class InboxTrackingService : IInboxTrackingService
|
||||
return null;
|
||||
}
|
||||
|
||||
if (metadata.LockedByAnotherUser)
|
||||
if (!string.IsNullOrWhiteSpace(metadata.OwnerUsername) &&
|
||||
!metadata.OwnedByCurrentUser)
|
||||
{
|
||||
return string.IsNullOrWhiteSpace(metadata.LastDownloadedByUsername)
|
||||
? "Importada por otro usuario"
|
||||
: $"Importada por {metadata.LastDownloadedByUsername}";
|
||||
return $"Propiedad de {metadata.OwnerUsername}";
|
||||
}
|
||||
|
||||
if (metadata.AlreadyInGestiona)
|
||||
@@ -593,15 +757,26 @@ public sealed class InboxTrackingService : IInboxTrackingService
|
||||
{
|
||||
public bool DownloadedByCurrentUser { get; init; }
|
||||
public bool DownloadedByAnotherUser { get; init; }
|
||||
public bool LockedByAnotherUser { get; init; }
|
||||
public string? LastDownloadedByUsername { get; init; }
|
||||
public DateTimeOffset? LastDownloadedAtUtc { get; init; }
|
||||
public DateTimeOffset? LastGestionaUploadAtUtc { get; init; }
|
||||
public bool AlreadyImported { get; init; }
|
||||
public bool AlreadyInGestiona { get; init; }
|
||||
public string? OwnerUsername { get; init; }
|
||||
public bool OwnedByCurrentUser { get; init; }
|
||||
public bool AccessibleByWorkGroup { get; init; }
|
||||
public IReadOnlyList<string> OwnerWorkGroups { get; init; } = [];
|
||||
public bool RequiresOwnerConfirmation =>
|
||||
!OwnedByCurrentUser &&
|
||||
AccessibleByWorkGroup &&
|
||||
!string.IsNullOrWhiteSpace(OwnerUsername);
|
||||
}
|
||||
}
|
||||
|
||||
private static bool IsLockedByAnotherUser(ReportDto report)
|
||||
=> report.AlreadyImported &&
|
||||
report.DownloadedByAnotherUser &&
|
||||
!report.DownloadedByCurrentUser;
|
||||
public sealed class ReportOwnershipException : InvalidOperationException
|
||||
{
|
||||
public ReportOwnershipException(string message)
|
||||
: base(message)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,6 +29,26 @@ public sealed class MySqlDenunciaStore : IDenunciaStore
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci
|
||||
""";
|
||||
|
||||
private const string GestionaUploadHistoryTableSql = """
|
||||
CREATE TABLE IF NOT EXISTS gestiona_upload_history (
|
||||
id BIGINT NOT NULL AUTO_INCREMENT,
|
||||
external_report_id INT NOT NULL,
|
||||
gestiona_file_url TEXT NOT NULL,
|
||||
gestiona_file_code VARCHAR(128) NOT NULL DEFAULT '',
|
||||
upload_type VARCHAR(64) NOT NULL DEFAULT '',
|
||||
assigned_group_code VARCHAR(32) NOT NULL DEFAULT '',
|
||||
uploaded_by_username VARCHAR(256) NOT NULL DEFAULT '',
|
||||
uploaded_at_utc DATETIME(6) NOT NULL,
|
||||
subject TEXT NULL,
|
||||
document_names TEXT NULL,
|
||||
created_at_utc DATETIME(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6),
|
||||
PRIMARY KEY (id),
|
||||
KEY ix_gestiona_upload_history_report (external_report_id),
|
||||
KEY ix_gestiona_upload_history_uploaded_at (uploaded_at_utc),
|
||||
KEY ix_gestiona_upload_history_user (uploaded_by_username)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci
|
||||
""";
|
||||
|
||||
private const string ComplaintSelectColumns = """
|
||||
external_registry_id,
|
||||
external_report_id,
|
||||
@@ -94,6 +114,9 @@ public sealed class MySqlDenunciaStore : IDenunciaStore
|
||||
workflow_status,
|
||||
selected_document_name,
|
||||
gestiona_uploaded_at_utc,
|
||||
gestiona_last_upload_type,
|
||||
gestiona_assigned_group,
|
||||
pending_update_source,
|
||||
is_in_gestiona,
|
||||
is_rejected,
|
||||
key_date,
|
||||
@@ -118,6 +141,23 @@ public sealed class MySqlDenunciaStore : IDenunciaStore
|
||||
a.encrypted_at_utc
|
||||
""";
|
||||
|
||||
private const string AttachmentMetadataSelectColumns = """
|
||||
a.id,
|
||||
a.attachment_type_id,
|
||||
a.description,
|
||||
a.attachment_date_utc,
|
||||
a.notes,
|
||||
c.external_report_id,
|
||||
a.original_file_name,
|
||||
NULL AS content,
|
||||
a.content_sha256,
|
||||
a.uploaded_to_gestiona,
|
||||
a.uploaded_at_utc,
|
||||
a.key_date,
|
||||
a.encryption_scheme,
|
||||
a.encrypted_at_utc
|
||||
""";
|
||||
|
||||
private static readonly (string Table, string Column, string Definition)[] SchemaColumnsToEnsure =
|
||||
[
|
||||
("complaints", "gestiona_file_code", "`gestiona_file_code` VARCHAR(128) NOT NULL DEFAULT ''"),
|
||||
@@ -143,6 +183,10 @@ public sealed class MySqlDenunciaStore : IDenunciaStore
|
||||
("complaints", "key_date", "`key_date` DATE NULL"),
|
||||
("complaints", "encryption_scheme", "`encryption_scheme` VARCHAR(64) NOT NULL DEFAULT 'none'"),
|
||||
("complaints", "encrypted_at_utc", "`encrypted_at_utc` DATETIME(6) NULL"),
|
||||
("complaints", "gestiona_last_upload_type", "`gestiona_last_upload_type` VARCHAR(64) NOT NULL DEFAULT ''"),
|
||||
("complaints", "gestiona_assigned_group", "`gestiona_assigned_group` VARCHAR(255) NOT NULL DEFAULT ''"),
|
||||
("complaints", "pending_update_source", "`pending_update_source` VARCHAR(256) NOT NULL DEFAULT ''"),
|
||||
("inbox_reports", "owner_user_id", "`owner_user_id` BIGINT NULL"),
|
||||
("complaint_attachments", "content_sha256", "`content_sha256` CHAR(64) NOT NULL DEFAULT ''"),
|
||||
("complaint_attachments", "key_date", "`key_date` DATE NULL"),
|
||||
("complaint_attachments", "encryption_scheme", "`encryption_scheme` VARCHAR(64) NOT NULL DEFAULT 'none'"),
|
||||
@@ -154,6 +198,7 @@ public sealed class MySqlDenunciaStore : IDenunciaStore
|
||||
("complaint_attachments", "ix_attachments_sha256", "INDEX `ix_attachments_sha256` (`content_sha256`)"),
|
||||
("complaints", "ix_complaints_key_date", "INDEX `ix_complaints_key_date` (`key_date`)"),
|
||||
("complaints", "ix_complaints_flags", "INDEX `ix_complaints_flags` (`is_update`, `is_in_gestiona`, `is_rejected`)"),
|
||||
("inbox_reports", "ix_inbox_reports_owner", "INDEX `ix_inbox_reports_owner` (`owner_user_id`)"),
|
||||
("complaint_attachments", "ix_attachments_key_date", "INDEX `ix_attachments_key_date` (`key_date`)"),
|
||||
];
|
||||
|
||||
@@ -211,6 +256,8 @@ public sealed class MySqlDenunciaStore : IDenunciaStore
|
||||
("complaints", "`display_name` TEXT NOT NULL"),
|
||||
("complaints", "`workflow_status` TEXT NOT NULL"),
|
||||
("complaints", "`selected_document_name` TEXT NULL"),
|
||||
("complaints", "`gestiona_last_upload_type` TEXT NOT NULL"),
|
||||
("complaints", "`gestiona_assigned_group` TEXT NOT NULL"),
|
||||
("complaint_attachments", "`description` TEXT NULL"),
|
||||
("complaint_attachments", "`notes` TEXT NOT NULL"),
|
||||
];
|
||||
@@ -422,6 +469,68 @@ public sealed class MySqlDenunciaStore : IDenunciaStore
|
||||
return result;
|
||||
}
|
||||
|
||||
public async Task<List<FicherosDenuncias>> GetFicherosMetadataByDenunciaAsync(
|
||||
int denunciaId,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
await EnsureSchemaReadyAsync(cancellationToken);
|
||||
|
||||
var sql = $"""
|
||||
SELECT
|
||||
{AttachmentMetadataSelectColumns}
|
||||
FROM complaint_attachments a
|
||||
INNER JOIN complaints c ON c.id = a.complaint_id
|
||||
WHERE c.external_report_id = @denunciaId
|
||||
ORDER BY a.original_file_name ASC;
|
||||
""";
|
||||
|
||||
await using var connection = await OpenConnectionAsync(cancellationToken);
|
||||
await using var command = new MySqlCommand(sql, connection);
|
||||
command.Parameters.AddWithValue("@denunciaId", denunciaId);
|
||||
|
||||
var result = new List<FicherosDenuncias>();
|
||||
await using var reader = await command.ExecuteReaderAsync(cancellationToken);
|
||||
while (await reader.ReadAsync(cancellationToken))
|
||||
{
|
||||
result.Add(MapAttachment(reader));
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public async Task<List<GestionaUploadHistoryEntry>> GetGestionaUploadHistoryAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
await EnsureSchemaReadyAsync(cancellationToken);
|
||||
|
||||
const string sql = """
|
||||
SELECT
|
||||
id,
|
||||
external_report_id,
|
||||
gestiona_file_url,
|
||||
gestiona_file_code,
|
||||
upload_type,
|
||||
assigned_group_code,
|
||||
uploaded_by_username,
|
||||
uploaded_at_utc,
|
||||
subject,
|
||||
document_names
|
||||
FROM gestiona_upload_history
|
||||
ORDER BY uploaded_at_utc DESC, id DESC;
|
||||
""";
|
||||
|
||||
await using var connection = await OpenConnectionAsync(cancellationToken);
|
||||
await using var command = new MySqlCommand(sql, connection);
|
||||
|
||||
var result = new List<GestionaUploadHistoryEntry>();
|
||||
await using var reader = await command.ExecuteReaderAsync(cancellationToken);
|
||||
while (await reader.ReadAsync(cancellationToken))
|
||||
{
|
||||
result.Add(MapGestionaUploadHistory(reader));
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public async Task<DenunciasGestiona?> GetDenunciaByIdAsync(
|
||||
int denunciaId,
|
||||
CancellationToken cancellationToken = default)
|
||||
@@ -446,6 +555,92 @@ public sealed class MySqlDenunciaStore : IDenunciaStore
|
||||
: null;
|
||||
}
|
||||
|
||||
public async Task<DenunciasGestiona?> GetDenunciaByGestionaFileCodeAsync(
|
||||
string gestionaFileCode,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
await EnsureSchemaReadyAsync(cancellationToken);
|
||||
|
||||
var sql = $"""
|
||||
SELECT
|
||||
{ComplaintSelectColumns}
|
||||
FROM complaints
|
||||
WHERE external_report_id = (
|
||||
SELECT history.external_report_id
|
||||
FROM gestiona_upload_history history
|
||||
WHERE history.gestiona_file_code = @gestionaFileCode
|
||||
ORDER BY history.uploaded_at_utc DESC, history.id DESC
|
||||
LIMIT 1
|
||||
)
|
||||
OR gestiona_file_code = @gestionaFileCode
|
||||
ORDER BY COALESCE(gestiona_uploaded_at_utc, report_date_utc) DESC, external_report_id DESC
|
||||
LIMIT 1;
|
||||
""";
|
||||
|
||||
await using var connection = await OpenConnectionAsync(cancellationToken);
|
||||
await using var command = new MySqlCommand(sql, connection);
|
||||
command.Parameters.AddWithValue("@gestionaFileCode", gestionaFileCode.Trim());
|
||||
|
||||
await using var reader = await command.ExecuteReaderAsync(cancellationToken);
|
||||
return await reader.ReadAsync(cancellationToken)
|
||||
? MapComplaint(reader)
|
||||
: null;
|
||||
}
|
||||
|
||||
public async Task AddGestionaUploadHistoryAsync(
|
||||
GestionaUploadHistoryCreateRequest request,
|
||||
string username,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
await EnsureSchemaReadyAsync(cancellationToken);
|
||||
|
||||
const string sql = """
|
||||
INSERT INTO gestiona_upload_history (
|
||||
external_report_id,
|
||||
gestiona_file_url,
|
||||
gestiona_file_code,
|
||||
upload_type,
|
||||
assigned_group_code,
|
||||
uploaded_by_username,
|
||||
uploaded_at_utc,
|
||||
subject,
|
||||
document_names
|
||||
)
|
||||
SELECT
|
||||
@externalReportId,
|
||||
@gestionaFileUrl,
|
||||
@gestionaFileCode,
|
||||
@uploadType,
|
||||
@assignedGroupCode,
|
||||
@uploadedByUsername,
|
||||
@uploadedAtUtc,
|
||||
@subject,
|
||||
@documentNames
|
||||
WHERE NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM gestiona_upload_history
|
||||
WHERE external_report_id = @externalReportId
|
||||
AND uploaded_at_utc = @uploadedAtUtc
|
||||
AND upload_type = @uploadType
|
||||
LIMIT 1
|
||||
);
|
||||
""";
|
||||
|
||||
await using var connection = await OpenConnectionAsync(cancellationToken);
|
||||
await using var command = new MySqlCommand(sql, connection);
|
||||
command.Parameters.AddWithValue("@externalReportId", request.DenunciaId);
|
||||
command.Parameters.AddWithValue("@gestionaFileUrl", request.ExpedienteGestionaUrl ?? string.Empty);
|
||||
command.Parameters.AddWithValue("@gestionaFileCode", request.CodigoExpedienteGestiona ?? string.Empty);
|
||||
command.Parameters.AddWithValue("@uploadType", request.TipoSubida ?? string.Empty);
|
||||
command.Parameters.AddWithValue("@assignedGroupCode", ExtractGroupCode(request.GrupoAsignado));
|
||||
command.Parameters.AddWithValue("@uploadedByUsername", string.IsNullOrWhiteSpace(username) ? "(sin usuario)" : username.Trim());
|
||||
command.Parameters.AddWithValue("@uploadedAtUtc", request.UploadedAtUtc == DateTime.MinValue ? DateTime.UtcNow : request.UploadedAtUtc);
|
||||
command.Parameters.AddWithValue("@subject", ToDbStringOrNull(request.Asunto));
|
||||
command.Parameters.AddWithValue("@documentNames", ToDbStringOrNull(request.Documentos));
|
||||
|
||||
await command.ExecuteNonQueryAsync(cancellationToken);
|
||||
}
|
||||
|
||||
public async Task UpsertDenunciaAsync(DenunciasGestiona denuncia, CancellationToken cancellationToken = default)
|
||||
{
|
||||
await EnsureSchemaReadyAsync(cancellationToken);
|
||||
@@ -516,6 +711,9 @@ public sealed class MySqlDenunciaStore : IDenunciaStore
|
||||
workflow_status,
|
||||
selected_document_name,
|
||||
gestiona_uploaded_at_utc,
|
||||
gestiona_last_upload_type,
|
||||
gestiona_assigned_group,
|
||||
pending_update_source,
|
||||
is_in_gestiona,
|
||||
is_rejected,
|
||||
key_date,
|
||||
@@ -586,6 +784,9 @@ public sealed class MySqlDenunciaStore : IDenunciaStore
|
||||
@workflowStatus,
|
||||
@selectedDocumentName,
|
||||
@gestionaUploadedAtUtc,
|
||||
@gestionaLastUploadType,
|
||||
@gestionaAssignedGroup,
|
||||
@pendingUpdateSource,
|
||||
@isInGestiona,
|
||||
@isRejected,
|
||||
@keyDate,
|
||||
@@ -656,6 +857,9 @@ public sealed class MySqlDenunciaStore : IDenunciaStore
|
||||
workflow_status = VALUES(workflow_status),
|
||||
selected_document_name = VALUES(selected_document_name),
|
||||
gestiona_uploaded_at_utc = VALUES(gestiona_uploaded_at_utc),
|
||||
gestiona_last_upload_type = VALUES(gestiona_last_upload_type),
|
||||
gestiona_assigned_group = VALUES(gestiona_assigned_group),
|
||||
pending_update_source = VALUES(pending_update_source),
|
||||
is_in_gestiona = VALUES(is_in_gestiona),
|
||||
is_rejected = VALUES(is_rejected),
|
||||
key_date = VALUES(key_date),
|
||||
@@ -731,6 +935,9 @@ public sealed class MySqlDenunciaStore : IDenunciaStore
|
||||
command.Parameters.AddWithValue("@workflowStatus", denuncia.EstadoDenuncia ?? string.Empty);
|
||||
command.Parameters.AddWithValue("@selectedDocumentName", ToDbStringOrNull(denuncia.ArchivoElegido));
|
||||
command.Parameters.AddWithValue("@gestionaUploadedAtUtc", ToDbDate(denuncia.FechaSubidaAGestiona));
|
||||
command.Parameters.AddWithValue("@gestionaLastUploadType", denuncia.UltimaSubidaGestionaTipo ?? string.Empty);
|
||||
command.Parameters.AddWithValue("@gestionaAssignedGroup", denuncia.UltimoGrupoAsignadoGestiona ?? string.Empty);
|
||||
command.Parameters.AddWithValue("@pendingUpdateSource", denuncia.PendingUpdateSource ?? string.Empty);
|
||||
command.Parameters.AddWithValue("@isInGestiona", denuncia.EnGestiona);
|
||||
command.Parameters.AddWithValue("@isRejected", denuncia.EnRechazada);
|
||||
command.Parameters.AddWithValue("@keyDate", ToDbDate(denuncia.KeyDate));
|
||||
@@ -795,19 +1002,17 @@ public sealed class MySqlDenunciaStore : IDenunciaStore
|
||||
description = @description,
|
||||
attachment_date_utc = @attachmentDateUtc,
|
||||
notes = @notes,
|
||||
content = @content,
|
||||
content_mime_type = @contentMimeType,
|
||||
content_sha256 = @contentSha256,
|
||||
uploaded_to_gestiona = CASE
|
||||
WHEN LOWER(@originalFileName) = 'report.txt' OR LOWER(@originalFileName) = 'report.pdf' THEN @uploadedToGestiona
|
||||
WHEN complaint_attachments.content_sha256 = @contentSha256 THEN complaint_attachments.uploaded_to_gestiona
|
||||
ELSE @uploadedToGestiona
|
||||
END,
|
||||
uploaded_at_utc = CASE
|
||||
WHEN LOWER(@originalFileName) = 'report.txt' OR LOWER(@originalFileName) = 'report.pdf' THEN @uploadedAtUtc
|
||||
WHEN complaint_attachments.content_sha256 = @contentSha256 THEN complaint_attachments.uploaded_at_utc
|
||||
ELSE @uploadedAtUtc
|
||||
END,
|
||||
content = @content,
|
||||
content_mime_type = @contentMimeType,
|
||||
content_sha256 = @contentSha256,
|
||||
key_date = @keyDate,
|
||||
encryption_scheme = @encryptionScheme,
|
||||
encrypted_at_utc = @encryptedAtUtc,
|
||||
@@ -1125,6 +1330,7 @@ public sealed class MySqlDenunciaStore : IDenunciaStore
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
await EnsureAttachmentChunksTableAsync(connection, cancellationToken);
|
||||
await EnsureGestionaUploadHistoryTableAsync(connection, cancellationToken);
|
||||
|
||||
foreach (var (table, column, definition) in SchemaColumnsToEnsure)
|
||||
{
|
||||
@@ -1156,6 +1362,54 @@ public sealed class MySqlDenunciaStore : IDenunciaStore
|
||||
await alterCommand.ExecuteNonQueryAsync(cancellationToken);
|
||||
}
|
||||
}
|
||||
|
||||
await BackfillReportOwnersAsync(connection, cancellationToken);
|
||||
}
|
||||
|
||||
private static async Task BackfillReportOwnersAsync(
|
||||
MySqlConnection connection,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
const string sql = """
|
||||
UPDATE inbox_reports ir
|
||||
LEFT JOIN (
|
||||
SELECT
|
||||
uir.inbox_report_id,
|
||||
CAST(
|
||||
SUBSTRING_INDEX(
|
||||
GROUP_CONCAT(
|
||||
uir.app_user_id
|
||||
ORDER BY
|
||||
COALESCE(
|
||||
uir.first_downloaded_at_utc,
|
||||
uir.last_downloaded_at_utc,
|
||||
uir.first_seen_at_utc
|
||||
),
|
||||
uir.app_user_id
|
||||
SEPARATOR ','
|
||||
),
|
||||
',',
|
||||
1
|
||||
) AS UNSIGNED
|
||||
) AS first_owner_user_id
|
||||
FROM user_inbox_reports uir
|
||||
WHERE uir.first_downloaded_at_utc IS NOT NULL
|
||||
OR uir.last_downloaded_at_utc IS NOT NULL
|
||||
GROUP BY uir.inbox_report_id
|
||||
) first_owner ON first_owner.inbox_report_id = ir.id
|
||||
SET ir.owner_user_id = COALESCE(
|
||||
first_owner.first_owner_user_id,
|
||||
ir.last_downloaded_by_user_id
|
||||
)
|
||||
WHERE ir.owner_user_id IS NULL
|
||||
AND (
|
||||
ir.imported_to_store_at_utc IS NOT NULL
|
||||
OR ir.imported_complaint_report_id IS NOT NULL
|
||||
);
|
||||
""";
|
||||
|
||||
await using var command = new MySqlCommand(sql, connection);
|
||||
await command.ExecuteNonQueryAsync(cancellationToken);
|
||||
}
|
||||
|
||||
private static async Task EnsureAttachmentChunksTableAsync(
|
||||
@@ -1167,6 +1421,15 @@ public sealed class MySqlDenunciaStore : IDenunciaStore
|
||||
await command.ExecuteNonQueryAsync(cancellationToken);
|
||||
}
|
||||
|
||||
private static async Task EnsureGestionaUploadHistoryTableAsync(
|
||||
MySqlConnection connection,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
await using var command = connection.CreateCommand();
|
||||
command.CommandText = GestionaUploadHistoryTableSql;
|
||||
await command.ExecuteNonQueryAsync(cancellationToken);
|
||||
}
|
||||
|
||||
private static string ExtractColumnName(string definition)
|
||||
{
|
||||
var first = definition.IndexOf('`');
|
||||
@@ -1462,6 +1725,9 @@ public sealed class MySqlDenunciaStore : IDenunciaStore
|
||||
EstadoDenuncia = GetString(record, "workflow_status"),
|
||||
ArchivoElegido = GetString(record, "selected_document_name"),
|
||||
FechaSubidaAGestiona = GetDateTime(record, "gestiona_uploaded_at_utc"),
|
||||
UltimaSubidaGestionaTipo = GetString(record, "gestiona_last_upload_type"),
|
||||
UltimoGrupoAsignadoGestiona = GetString(record, "gestiona_assigned_group"),
|
||||
PendingUpdateSource = GetString(record, "pending_update_source"),
|
||||
EnGestiona = GetBoolean(record, "is_in_gestiona"),
|
||||
EnRechazada = GetBoolean(record, "is_rejected"),
|
||||
KeyDate = GetNullableDateOnly(record, "key_date"),
|
||||
@@ -1491,6 +1757,33 @@ public sealed class MySqlDenunciaStore : IDenunciaStore
|
||||
};
|
||||
}
|
||||
|
||||
private static GestionaUploadHistoryEntry MapGestionaUploadHistory(IDataRecord record)
|
||||
{
|
||||
return new GestionaUploadHistoryEntry(
|
||||
Convert.ToInt64(record["id"]),
|
||||
GetInt32(record, "external_report_id"),
|
||||
GetString(record, "gestiona_file_url"),
|
||||
GetString(record, "gestiona_file_code"),
|
||||
GetString(record, "upload_type"),
|
||||
ExtractGroupCode(GetString(record, "assigned_group_code")),
|
||||
GetString(record, "uploaded_by_username"),
|
||||
GetDateTime(record, "uploaded_at_utc"),
|
||||
GetString(record, "subject"),
|
||||
GetString(record, "document_names"));
|
||||
}
|
||||
|
||||
private static string ExtractGroupCode(string? value)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(value))
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
var trimmed = value.Trim();
|
||||
var code = new string(trimmed.TakeWhile(char.IsDigit).ToArray());
|
||||
return string.IsNullOrWhiteSpace(code) ? trimmed : code;
|
||||
}
|
||||
|
||||
private static object ToDbDate(DateTime value)
|
||||
{
|
||||
return value == DateTime.MinValue ? DBNull.Value : value;
|
||||
|
||||
@@ -12,43 +12,163 @@ public sealed class UserComplaintAccessService
|
||||
_connectionStringProvider = connectionStringProvider;
|
||||
}
|
||||
|
||||
public async Task<HashSet<int>> GetAllowedComplaintIdsAsync(string username, CancellationToken cancellationToken = default)
|
||||
public async Task<HashSet<int>> GetAllowedComplaintIdsAsync(
|
||||
string username,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var access = await GetComplaintAccessAsync(username, null, cancellationToken);
|
||||
return access.Keys.ToHashSet();
|
||||
}
|
||||
|
||||
public async Task<IReadOnlyDictionary<int, ComplaintAccessInfo>> GetComplaintAccessAsync(
|
||||
string username,
|
||||
IReadOnlyCollection<int>? complaintIds = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(username))
|
||||
{
|
||||
return [];
|
||||
return new Dictionary<int, ComplaintAccessInfo>();
|
||||
}
|
||||
|
||||
const string sql = """
|
||||
SELECT DISTINCT ir.imported_complaint_report_id
|
||||
FROM inbox_reports ir
|
||||
INNER JOIN user_inbox_reports uir ON uir.inbox_report_id = ir.id
|
||||
INNER JOIN app_users au ON au.id = uir.app_user_id
|
||||
WHERE au.username = @username
|
||||
AND ir.imported_complaint_report_id IS NOT NULL
|
||||
AND uir.download_count > 0;
|
||||
""";
|
||||
|
||||
var connectionString = await _connectionStringProvider.GetConnectionStringAsync(cancellationToken);
|
||||
await using var connection = new MySqlConnection(connectionString);
|
||||
await connection.OpenAsync(cancellationToken);
|
||||
|
||||
await using var command = new MySqlCommand(sql, connection);
|
||||
await using var command = connection.CreateCommand();
|
||||
command.Parameters.AddWithValue("@username", username.Trim());
|
||||
|
||||
var result = new HashSet<int>();
|
||||
var idFilter = string.Empty;
|
||||
if (complaintIds is { Count: > 0 })
|
||||
{
|
||||
var parameters = new List<string>(complaintIds.Count);
|
||||
var index = 0;
|
||||
foreach (var complaintId in complaintIds.Where(id => id > 0).Distinct())
|
||||
{
|
||||
var parameterName = $"@complaintId{index++}";
|
||||
parameters.Add(parameterName);
|
||||
command.Parameters.AddWithValue(parameterName, complaintId);
|
||||
}
|
||||
|
||||
if (parameters.Count == 0)
|
||||
{
|
||||
return new Dictionary<int, ComplaintAccessInfo>();
|
||||
}
|
||||
|
||||
idFilter = $"AND COALESCE(ir.imported_complaint_report_id, ir.progressive_id) IN ({string.Join(", ", parameters)})";
|
||||
}
|
||||
|
||||
command.CommandText = $"""
|
||||
SELECT
|
||||
COALESCE(ir.imported_complaint_report_id, ir.progressive_id) AS complaint_id,
|
||||
owner.username AS owner_username,
|
||||
CASE WHEN ir.owner_user_id = viewer.id THEN 1 ELSE 0 END AS owned_by_current_user,
|
||||
CASE
|
||||
WHEN ir.owner_user_id IS NULL THEN 0
|
||||
WHEN EXISTS (
|
||||
SELECT 1
|
||||
FROM app_user_groups shared_membership
|
||||
INNER JOIN work_groups active_group
|
||||
ON active_group.id = shared_membership.work_group_id
|
||||
AND active_group.is_active = 1
|
||||
WHERE shared_membership.app_user_id IN (viewer.id, ir.owner_user_id)
|
||||
GROUP BY shared_membership.work_group_id
|
||||
HAVING COUNT(DISTINCT shared_membership.app_user_id) = 2
|
||||
) THEN 1
|
||||
ELSE 0
|
||||
END AS accessible_by_group,
|
||||
(
|
||||
SELECT GROUP_CONCAT(DISTINCT wg.code ORDER BY wg.code SEPARATOR ',')
|
||||
FROM app_user_groups owner_membership
|
||||
INNER JOIN work_groups wg
|
||||
ON wg.id = owner_membership.work_group_id
|
||||
AND wg.is_active = 1
|
||||
WHERE owner_membership.app_user_id = ir.owner_user_id
|
||||
) AS owner_group_codes
|
||||
FROM app_users viewer
|
||||
INNER JOIN inbox_reports ir
|
||||
ON COALESCE(ir.imported_complaint_report_id, ir.progressive_id) IS NOT NULL
|
||||
LEFT JOIN app_users owner ON owner.id = ir.owner_user_id
|
||||
LEFT JOIN user_inbox_reports current_tracking
|
||||
ON current_tracking.inbox_report_id = ir.id
|
||||
AND current_tracking.app_user_id = viewer.id
|
||||
WHERE viewer.username = @username
|
||||
{idFilter}
|
||||
AND (
|
||||
ir.owner_user_id = viewer.id
|
||||
OR EXISTS (
|
||||
SELECT 1
|
||||
FROM app_user_groups shared_membership
|
||||
INNER JOIN work_groups active_group
|
||||
ON active_group.id = shared_membership.work_group_id
|
||||
AND active_group.is_active = 1
|
||||
WHERE shared_membership.app_user_id IN (viewer.id, ir.owner_user_id)
|
||||
GROUP BY shared_membership.work_group_id
|
||||
HAVING COUNT(DISTINCT shared_membership.app_user_id) = 2
|
||||
)
|
||||
OR (
|
||||
ir.owner_user_id IS NULL
|
||||
AND current_tracking.download_count > 0
|
||||
)
|
||||
);
|
||||
""";
|
||||
|
||||
var result = new Dictionary<int, ComplaintAccessInfo>();
|
||||
await using var reader = await command.ExecuteReaderAsync(cancellationToken);
|
||||
while (await reader.ReadAsync(cancellationToken))
|
||||
{
|
||||
result.Add(Convert.ToInt32(reader.GetValue(0), CultureInfo.InvariantCulture));
|
||||
var complaintId = Convert.ToInt32(
|
||||
reader.GetValue(reader.GetOrdinal("complaint_id")),
|
||||
CultureInfo.InvariantCulture);
|
||||
var ownerUsername = reader.IsDBNull(reader.GetOrdinal("owner_username"))
|
||||
? string.Empty
|
||||
: reader.GetString(reader.GetOrdinal("owner_username"));
|
||||
var ownedByCurrentUser =
|
||||
reader.GetInt32(reader.GetOrdinal("owned_by_current_user")) == 1;
|
||||
var accessibleByGroup =
|
||||
reader.GetInt32(reader.GetOrdinal("accessible_by_group")) == 1;
|
||||
var groupCodes = reader.IsDBNull(reader.GetOrdinal("owner_group_codes"))
|
||||
? []
|
||||
: reader.GetString(reader.GetOrdinal("owner_group_codes"))
|
||||
.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
|
||||
|
||||
result[complaintId] = new ComplaintAccessInfo(
|
||||
complaintId,
|
||||
ownerUsername,
|
||||
ownedByCurrentUser,
|
||||
accessibleByGroup,
|
||||
groupCodes);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public async Task<bool> CanAccessComplaintAsync(string username, int complaintId, CancellationToken cancellationToken = default)
|
||||
public async Task<bool> CanAccessComplaintAsync(
|
||||
string username,
|
||||
int complaintId,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var allowedIds = await GetAllowedComplaintIdsAsync(username, cancellationToken);
|
||||
return allowedIds.Contains(complaintId);
|
||||
if (complaintId <= 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var access = await GetComplaintAccessAsync(
|
||||
username,
|
||||
[complaintId],
|
||||
cancellationToken);
|
||||
return access.ContainsKey(complaintId);
|
||||
}
|
||||
}
|
||||
|
||||
public sealed record ComplaintAccessInfo(
|
||||
int ComplaintId,
|
||||
string OwnerUsername,
|
||||
bool OwnedByCurrentUser,
|
||||
bool AccessibleByGroup,
|
||||
IReadOnlyList<string> OwnerGroupCodes)
|
||||
{
|
||||
public bool RequiresOwnerConfirmation =>
|
||||
!OwnedByCurrentUser &&
|
||||
AccessibleByGroup &&
|
||||
!string.IsNullOrWhiteSpace(OwnerUsername);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,414 @@
|
||||
using System.Globalization;
|
||||
using GestionaDenuncias.Shared.Models;
|
||||
using MySqlConnector;
|
||||
|
||||
namespace ApiDenuncias.Services;
|
||||
|
||||
public sealed class WorkGroupAdministrationService
|
||||
{
|
||||
private readonly IDenunciaStore _denunciaStore;
|
||||
private readonly MySqlConnectionStringProvider _connectionStringProvider;
|
||||
|
||||
public WorkGroupAdministrationService(
|
||||
IDenunciaStore denunciaStore,
|
||||
MySqlConnectionStringProvider connectionStringProvider)
|
||||
{
|
||||
_denunciaStore = denunciaStore;
|
||||
_connectionStringProvider = connectionStringProvider;
|
||||
}
|
||||
|
||||
public async Task<WorkGroupAdministrationDto> GetAsync(
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
await _denunciaStore.EnsureSchemaAsync(cancellationToken);
|
||||
await using var connection = await OpenConnectionAsync(cancellationToken);
|
||||
|
||||
var groups = await LoadGroupsAsync(connection, cancellationToken);
|
||||
var users = new Dictionary<long, UserGroupBuilder>();
|
||||
|
||||
const string sql = """
|
||||
SELECT
|
||||
au.id,
|
||||
au.username,
|
||||
wg.code AS group_code
|
||||
FROM app_users au
|
||||
LEFT JOIN app_user_groups aug ON aug.app_user_id = au.id
|
||||
LEFT JOIN work_groups wg
|
||||
ON wg.id = aug.work_group_id
|
||||
AND wg.is_active = 1
|
||||
ORDER BY au.username, wg.code;
|
||||
""";
|
||||
|
||||
await using var command = new MySqlCommand(sql, connection);
|
||||
await using var reader = await command.ExecuteReaderAsync(cancellationToken);
|
||||
while (await reader.ReadAsync(cancellationToken))
|
||||
{
|
||||
var userId = reader.GetInt64(reader.GetOrdinal("id"));
|
||||
if (!users.TryGetValue(userId, out var user))
|
||||
{
|
||||
user = new UserGroupBuilder(
|
||||
userId,
|
||||
reader.GetString(reader.GetOrdinal("username")));
|
||||
users[userId] = user;
|
||||
}
|
||||
|
||||
var groupOrdinal = reader.GetOrdinal("group_code");
|
||||
if (!reader.IsDBNull(groupOrdinal))
|
||||
{
|
||||
user.GroupCodes.Add(reader.GetString(groupOrdinal));
|
||||
}
|
||||
}
|
||||
|
||||
return new WorkGroupAdministrationDto(
|
||||
groups,
|
||||
users.Values
|
||||
.Select(user => new UserWorkGroupDto(
|
||||
user.UserId,
|
||||
user.Username,
|
||||
user.GroupCodes.OrderBy(code => code, StringComparer.Ordinal).ToArray()))
|
||||
.ToArray());
|
||||
}
|
||||
|
||||
public async Task<CurrentUserWorkGroupsDto> GetUserGroupsAsync(
|
||||
string username,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
await _denunciaStore.EnsureSchemaAsync(cancellationToken);
|
||||
|
||||
var normalizedUsername = username?.Trim();
|
||||
if (string.IsNullOrWhiteSpace(normalizedUsername))
|
||||
{
|
||||
throw new InvalidOperationException("No hay usuario autenticado.");
|
||||
}
|
||||
|
||||
await using var connection = await OpenConnectionAsync(cancellationToken);
|
||||
const string sql = """
|
||||
SELECT wg.code
|
||||
FROM app_users au
|
||||
INNER JOIN app_user_groups aug ON aug.app_user_id = au.id
|
||||
INNER JOIN work_groups wg
|
||||
ON wg.id = aug.work_group_id
|
||||
AND wg.is_active = 1
|
||||
WHERE LOWER(au.username) = LOWER(@username)
|
||||
ORDER BY wg.code;
|
||||
""";
|
||||
|
||||
var groupCodes = new List<string>();
|
||||
await using var command = new MySqlCommand(sql, connection);
|
||||
command.Parameters.AddWithValue("@username", normalizedUsername);
|
||||
await using var reader = await command.ExecuteReaderAsync(cancellationToken);
|
||||
while (await reader.ReadAsync(cancellationToken))
|
||||
{
|
||||
groupCodes.Add(reader.GetString(reader.GetOrdinal("code")));
|
||||
}
|
||||
|
||||
return new CurrentUserWorkGroupsDto(normalizedUsername, groupCodes);
|
||||
}
|
||||
|
||||
public async Task<WorkGroupAdministrationDto> UpdateUserGroupsAsync(
|
||||
string username,
|
||||
IEnumerable<string> groupCodes,
|
||||
string changedByUsername,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
await _denunciaStore.EnsureSchemaAsync(cancellationToken);
|
||||
|
||||
var normalizedUsername = username?.Trim();
|
||||
if (string.IsNullOrWhiteSpace(normalizedUsername))
|
||||
{
|
||||
throw new InvalidOperationException("Debes indicar el usuario que se va a configurar.");
|
||||
}
|
||||
|
||||
var normalizedCodes = groupCodes
|
||||
.Where(code => !string.IsNullOrWhiteSpace(code))
|
||||
.Select(code => code.Trim())
|
||||
.Distinct(StringComparer.OrdinalIgnoreCase)
|
||||
.ToArray();
|
||||
|
||||
if (normalizedCodes.Length == 0)
|
||||
{
|
||||
throw new InvalidOperationException("Cada usuario debe pertenecer al menos a un grupo de trabajo.");
|
||||
}
|
||||
|
||||
await using var connection = await OpenConnectionAsync(cancellationToken);
|
||||
await using var transaction = await connection.BeginTransactionAsync(cancellationToken);
|
||||
try
|
||||
{
|
||||
var validGroups = await LoadGroupIdsAsync(
|
||||
connection,
|
||||
(MySqlTransaction)transaction,
|
||||
normalizedCodes,
|
||||
cancellationToken);
|
||||
|
||||
if (validGroups.Count != normalizedCodes.Length)
|
||||
{
|
||||
throw new InvalidOperationException("Se ha indicado un grupo de trabajo que no existe o no esta activo.");
|
||||
}
|
||||
|
||||
var userId = await EnsureUserAsync(
|
||||
connection,
|
||||
(MySqlTransaction)transaction,
|
||||
normalizedUsername,
|
||||
cancellationToken);
|
||||
var existingGroupIds = await LoadUserGroupIdsAsync(
|
||||
connection,
|
||||
(MySqlTransaction)transaction,
|
||||
userId,
|
||||
cancellationToken);
|
||||
var desiredGroupIds = validGroups.Values.ToHashSet();
|
||||
|
||||
foreach (var removedGroupId in existingGroupIds.Except(desiredGroupIds))
|
||||
{
|
||||
await DeleteMembershipAsync(
|
||||
connection,
|
||||
(MySqlTransaction)transaction,
|
||||
userId,
|
||||
removedGroupId,
|
||||
cancellationToken);
|
||||
await AddHistoryAsync(
|
||||
connection,
|
||||
(MySqlTransaction)transaction,
|
||||
userId,
|
||||
removedGroupId,
|
||||
"removed",
|
||||
changedByUsername,
|
||||
cancellationToken);
|
||||
}
|
||||
|
||||
foreach (var addedGroupId in desiredGroupIds.Except(existingGroupIds))
|
||||
{
|
||||
await AddMembershipAsync(
|
||||
connection,
|
||||
(MySqlTransaction)transaction,
|
||||
userId,
|
||||
addedGroupId,
|
||||
changedByUsername,
|
||||
cancellationToken);
|
||||
await AddHistoryAsync(
|
||||
connection,
|
||||
(MySqlTransaction)transaction,
|
||||
userId,
|
||||
addedGroupId,
|
||||
"added",
|
||||
changedByUsername,
|
||||
cancellationToken);
|
||||
}
|
||||
|
||||
await transaction.CommitAsync(cancellationToken);
|
||||
}
|
||||
catch
|
||||
{
|
||||
await transaction.RollbackAsync(cancellationToken);
|
||||
throw;
|
||||
}
|
||||
|
||||
return await GetAsync(cancellationToken);
|
||||
}
|
||||
|
||||
private static async Task<List<WorkGroupDto>> LoadGroupsAsync(
|
||||
MySqlConnection connection,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
const string sql = """
|
||||
SELECT code, name
|
||||
FROM work_groups
|
||||
WHERE is_active = 1
|
||||
ORDER BY code;
|
||||
""";
|
||||
|
||||
var groups = new List<WorkGroupDto>();
|
||||
await using var command = new MySqlCommand(sql, connection);
|
||||
await using var reader = await command.ExecuteReaderAsync(cancellationToken);
|
||||
while (await reader.ReadAsync(cancellationToken))
|
||||
{
|
||||
groups.Add(new WorkGroupDto(
|
||||
reader.GetString(reader.GetOrdinal("code")),
|
||||
reader.GetString(reader.GetOrdinal("name"))));
|
||||
}
|
||||
|
||||
return groups;
|
||||
}
|
||||
|
||||
private static async Task<Dictionary<string, long>> LoadGroupIdsAsync(
|
||||
MySqlConnection connection,
|
||||
MySqlTransaction transaction,
|
||||
IReadOnlyList<string> codes,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var result = new Dictionary<string, long>(StringComparer.OrdinalIgnoreCase);
|
||||
await using var command = new MySqlCommand { Connection = connection, Transaction = transaction };
|
||||
var parameters = new List<string>(codes.Count);
|
||||
for (var index = 0; index < codes.Count; index++)
|
||||
{
|
||||
var parameterName = $"@code{index}";
|
||||
parameters.Add(parameterName);
|
||||
command.Parameters.AddWithValue(parameterName, codes[index]);
|
||||
}
|
||||
|
||||
command.CommandText = $"""
|
||||
SELECT id, code
|
||||
FROM work_groups
|
||||
WHERE is_active = 1
|
||||
AND code IN ({string.Join(", ", parameters)});
|
||||
""";
|
||||
|
||||
await using var reader = await command.ExecuteReaderAsync(cancellationToken);
|
||||
while (await reader.ReadAsync(cancellationToken))
|
||||
{
|
||||
result[reader.GetString(reader.GetOrdinal("code"))] =
|
||||
reader.GetInt64(reader.GetOrdinal("id"));
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private static async Task<long> EnsureUserAsync(
|
||||
MySqlConnection connection,
|
||||
MySqlTransaction transaction,
|
||||
string username,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
const string insertSql = """
|
||||
INSERT INTO app_users (username)
|
||||
VALUES (@username)
|
||||
ON DUPLICATE KEY UPDATE
|
||||
updated_at_utc = CURRENT_TIMESTAMP(6);
|
||||
""";
|
||||
|
||||
await using (var insert = new MySqlCommand(insertSql, connection, transaction))
|
||||
{
|
||||
insert.Parameters.AddWithValue("@username", username);
|
||||
await insert.ExecuteNonQueryAsync(cancellationToken);
|
||||
}
|
||||
|
||||
const string selectSql = """
|
||||
SELECT id
|
||||
FROM app_users
|
||||
WHERE username = @username
|
||||
LIMIT 1;
|
||||
""";
|
||||
|
||||
await using var select = new MySqlCommand(selectSql, connection, transaction);
|
||||
select.Parameters.AddWithValue("@username", username);
|
||||
var result = await select.ExecuteScalarAsync(cancellationToken);
|
||||
return Convert.ToInt64(result, CultureInfo.InvariantCulture);
|
||||
}
|
||||
|
||||
private static async Task<HashSet<long>> LoadUserGroupIdsAsync(
|
||||
MySqlConnection connection,
|
||||
MySqlTransaction transaction,
|
||||
long userId,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
const string sql = """
|
||||
SELECT work_group_id
|
||||
FROM app_user_groups
|
||||
WHERE app_user_id = @userId;
|
||||
""";
|
||||
|
||||
var result = new HashSet<long>();
|
||||
await using var command = new MySqlCommand(sql, connection, transaction);
|
||||
command.Parameters.AddWithValue("@userId", userId);
|
||||
await using var reader = await command.ExecuteReaderAsync(cancellationToken);
|
||||
while (await reader.ReadAsync(cancellationToken))
|
||||
{
|
||||
result.Add(reader.GetInt64(0));
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private static async Task DeleteMembershipAsync(
|
||||
MySqlConnection connection,
|
||||
MySqlTransaction transaction,
|
||||
long userId,
|
||||
long groupId,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
const string sql = """
|
||||
DELETE FROM app_user_groups
|
||||
WHERE app_user_id = @userId
|
||||
AND work_group_id = @groupId;
|
||||
""";
|
||||
|
||||
await using var command = new MySqlCommand(sql, connection, transaction);
|
||||
command.Parameters.AddWithValue("@userId", userId);
|
||||
command.Parameters.AddWithValue("@groupId", groupId);
|
||||
await command.ExecuteNonQueryAsync(cancellationToken);
|
||||
}
|
||||
|
||||
private static async Task AddMembershipAsync(
|
||||
MySqlConnection connection,
|
||||
MySqlTransaction transaction,
|
||||
long userId,
|
||||
long groupId,
|
||||
string changedByUsername,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
const string sql = """
|
||||
INSERT INTO app_user_groups (
|
||||
app_user_id,
|
||||
work_group_id,
|
||||
assigned_by_username
|
||||
) VALUES (
|
||||
@userId,
|
||||
@groupId,
|
||||
@changedByUsername
|
||||
)
|
||||
ON DUPLICATE KEY UPDATE
|
||||
assigned_by_username = VALUES(assigned_by_username);
|
||||
""";
|
||||
|
||||
await using var command = new MySqlCommand(sql, connection, transaction);
|
||||
command.Parameters.AddWithValue("@userId", userId);
|
||||
command.Parameters.AddWithValue("@groupId", groupId);
|
||||
command.Parameters.AddWithValue("@changedByUsername", changedByUsername?.Trim() ?? string.Empty);
|
||||
await command.ExecuteNonQueryAsync(cancellationToken);
|
||||
}
|
||||
|
||||
private static async Task AddHistoryAsync(
|
||||
MySqlConnection connection,
|
||||
MySqlTransaction transaction,
|
||||
long userId,
|
||||
long groupId,
|
||||
string action,
|
||||
string changedByUsername,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
const string sql = """
|
||||
INSERT INTO app_user_group_history (
|
||||
app_user_id,
|
||||
work_group_id,
|
||||
action,
|
||||
changed_by_username
|
||||
) VALUES (
|
||||
@userId,
|
||||
@groupId,
|
||||
@action,
|
||||
@changedByUsername
|
||||
);
|
||||
""";
|
||||
|
||||
await using var command = new MySqlCommand(sql, connection, transaction);
|
||||
command.Parameters.AddWithValue("@userId", userId);
|
||||
command.Parameters.AddWithValue("@groupId", groupId);
|
||||
command.Parameters.AddWithValue("@action", action);
|
||||
command.Parameters.AddWithValue("@changedByUsername", changedByUsername?.Trim() ?? string.Empty);
|
||||
await command.ExecuteNonQueryAsync(cancellationToken);
|
||||
}
|
||||
|
||||
private async Task<MySqlConnection> OpenConnectionAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
var connectionString = await _connectionStringProvider.GetConnectionStringAsync(cancellationToken);
|
||||
var connection = new MySqlConnection(connectionString);
|
||||
await connection.OpenAsync(cancellationToken);
|
||||
await using var command = new MySqlCommand("SET time_zone = '+00:00';", connection);
|
||||
await command.ExecuteNonQueryAsync(cancellationToken);
|
||||
return connection;
|
||||
}
|
||||
|
||||
private sealed record UserGroupBuilder(long UserId, string Username)
|
||||
{
|
||||
public HashSet<string> GroupCodes { get; } = new(StringComparer.OrdinalIgnoreCase);
|
||||
}
|
||||
}
|
||||
@@ -36,15 +36,21 @@
|
||||
"Gestiona": {
|
||||
"ApiBase": "https://02.g3stiona.com",
|
||||
"AccessToken": "_yr.xVvPOllsyd1TYZRxUxg__c",
|
||||
"ExternalProcedureId": "82722c9b-cecc-4299-8a7b-ce5abeb8170b",
|
||||
"CircuitTemplateId": "bb997758-7436-46ab-9dc3-50dce2e02cfa",
|
||||
"CircuitSignerStampHref": "https://02.g3stiona.com/rest/organ-stamps/3c6eaab4-7fcd-4b21-8676-bf8719be5d36",
|
||||
"ProcedureName": "Procedimiento test 2",
|
||||
"ExternalProcedureName": "",
|
||||
"ExternalProcedureSiaCode": "3109963",
|
||||
"ManagementUnitGroupCode": "700",
|
||||
"CircuitTemplateName": "Firma automatizada",
|
||||
"CircuitNewComplaintTemplateName": "CT-Nueva denuncia",
|
||||
"CircuitUpdateSajTemplateName": "CT-Actualización denuncia SAJ",
|
||||
"CircuitUpdateSdiTemplateName": "CT-Actualización denuncia SDI",
|
||||
"CircuitCommunicationSajTemplateName": "CT-Comunicación SAJ a denunciante",
|
||||
"CircuitCommunicationSdiTemplateName": "CT-Comunicación SDI a denunciante",
|
||||
"CircuitSignerStampTitle": "oaaf-complaints-tramit",
|
||||
"CircuitRecipientGroupHref": "https://02.g3stiona.com/rest/groups/454fa4ec-8b82-4240-9419-113f45d4b004",
|
||||
"CircuitVersion": "2",
|
||||
"UserLink": "https://02.g3stiona.com/rest/users/0c168833-8e27-4695-a301-b79924031f63",
|
||||
"GroupLink": "https://02.g3stiona.com/rest/groups/6dbfc433-1eb6-4b9a-a533-bfebc652c101",
|
||||
"Location": "2.02.01"
|
||||
"DocumentMetadataLanguage": "es",
|
||||
"DocumentMetadataType": "TD15",
|
||||
"DocumentMetadataSubtype": "TD15_011"
|
||||
},
|
||||
"GlobalLeaks": {
|
||||
"BaseUrl": "https://prebuzon.antifraudeandalucia.es",
|
||||
|
||||
@@ -308,7 +308,7 @@
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<div class="row" style="max-height:415px; overflow-y:auto; overflow-x:hidden">
|
||||
<div class="row">
|
||||
<Tabs @ref=tabsDtsPer NavStyle="NavStyle.VerticalUnderline" Class="col-md-2" style="margin-top:25px; font-size:12px; gap:0; width:165px; overflow-y:auto" OnShown="@(args => OnTabShownAsync(args))">
|
||||
<Tab Title="Nóminas" Name="tabNominas">
|
||||
<Content>
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
@inject UserState UserState
|
||||
|
||||
<div class="tablaTabLateral">
|
||||
@* <input type="button" value="Nueva maternidad/riesgo de embarazo" @onclick="@(() => abrirPopupModificacion(new MATERNIDADES(), true))" class="mb-2 btnOAAFBlack" /> *@
|
||||
<input type="button" value="Nueva diferencia Pago Delegado" @onclick="@(() => abrirPopupModificacion(new DIFERENCIAPAGODELEGADO(), true))" class="mb-2 btnOAAFBlack" />
|
||||
|
||||
<div style="overflow-x:auto;" class="tablaDesk">
|
||||
<Grid TItem="DIFERENCIAPAGODELEGADO"
|
||||
@@ -33,6 +33,9 @@
|
||||
PaginationItemsTextFormat="{0} - {1} de {2} elementos">
|
||||
|
||||
<GridColumns>
|
||||
<GridColumn TItem="DIFERENCIAPAGODELEGADO" HeaderText="">
|
||||
<button @onclick="@(() => abrirPopupModificacion(context, false))" class="btnOAAFAzul">Editar</button>
|
||||
</GridColumn>
|
||||
<GridColumn TItem="DIFERENCIAPAGODELEGADO" HeaderText="Fecha Inicio">
|
||||
@context.FECHAINICIO?.ToString("dd/MM/yyyy")
|
||||
</GridColumn>
|
||||
@@ -50,8 +53,7 @@
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@* <Modal @ref="popupGestionDatos" IsVerticallyCentered="true" UseStaticBackdrop="true" CloseOnEscape="false">
|
||||
<Modal @ref="popupGestionDatos" IsVerticallyCentered="true" UseStaticBackdrop="true" CloseOnEscape="false">
|
||||
<BodyTemplate>
|
||||
<div class="row">
|
||||
|
||||
@@ -64,28 +66,19 @@
|
||||
<input class="form-control" type="date" @bind-value="ItemEnEdicion.FECHAFIN" />
|
||||
</div>
|
||||
<div class="col-md-12 mb-2">
|
||||
<label for="txtEDesc" class="fw-bold">Base cotización seguridad social: </label>
|
||||
<input class="form-control" type="number" @bind-value="@ItemEnEdicion.BASECOTIZACIONSEGURIDADSOCIAL" />
|
||||
<label for="txtEDesc" class="fw-bold">Base Diaria Seguridad Social: </label>
|
||||
<input class="form-control" type="number" @bind-value="@ItemEnEdicion.BASEDIARIASEGURIDADSOCIAL" />
|
||||
</div>
|
||||
<div class="col-md-12 mb-2">
|
||||
<label for="txtEDesc" class="fw-bold">Porcentaje reducción jornada: </label>
|
||||
<input class="form-control" type="number" @bind-value="@ItemEnEdicion.PORCENTAJEREDUCCIONJORNADA" />
|
||||
<label for="txtEDesc" class="fw-bold">Base Pago Directo: </label>
|
||||
<input class="form-control" type="number" @bind-value="@ItemEnEdicion.BASEPAGODIRECTO" />
|
||||
</div>
|
||||
<div class="col-md-12" style="display:flex; justify-content:space-between">
|
||||
<label for="txtEDesc" class="fw-bold">Riesgo embarazo: </label>
|
||||
<input class="" type="checkbox" id="chbRiesgoEmbarazo" checked="@ItemEnEdicion.RIESGOEMBARAZO" />
|
||||
|
||||
<label for="txtEDesc" class="fw-bold">Nomina normal: </label>
|
||||
<input class="" type="checkbox" id="chbNominaNormal" checked="@ItemEnEdicion.NOMINANORMAL" />
|
||||
|
||||
<label for="txtEDesc" class="fw-bold">Nomina seguridad social: </label>
|
||||
<input class="" type="checkbox" id="chbNominaSS" checked="@ItemEnEdicion.NOMINASEGURIDADSOCIAL" />
|
||||
</div>
|
||||
</div>
|
||||
</BodyTemplate>
|
||||
<FooterTemplate>
|
||||
<Button Color="ButtonColor.Secondary" @onclick="cerrarPopupModificacion">Cerrar</Button>
|
||||
@if (ItemEnEdicion.IDMATERNIDADES != 0)
|
||||
@if (ItemEnEdicion.IDDIFERENCIAPAGODELEGADO != 0)
|
||||
{
|
||||
<Button Type="ButtonType.Submit" Color="ButtonColor.Primary" @onclick="@(() => GestionarDatos(false))">Modificar</Button>
|
||||
}
|
||||
@@ -95,8 +88,6 @@
|
||||
}
|
||||
</FooterTemplate>
|
||||
</Modal>
|
||||
*@
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -104,8 +95,10 @@
|
||||
[Parameter]
|
||||
public PERSONAS Persona { get; set; } = new PERSONAS();
|
||||
private HttpClient cliente = new HttpClient();
|
||||
private Modal popupGestionDatos = default;
|
||||
[Parameter]
|
||||
public EventCallback OnPersonaActualizada { get; set; }
|
||||
private DIFERENCIAPAGODELEGADO ItemEnEdicion { get; set; } = new DIFERENCIAPAGODELEGADO();
|
||||
// private List<int?> meses = new List<int?>();
|
||||
private List<DIFERENCIAPAGODELEGADO> itmList = new List<DIFERENCIAPAGODELEGADO>();
|
||||
|
||||
@@ -129,5 +122,45 @@
|
||||
Console.WriteLine($"Error al obtener los datos: {e.Message}");
|
||||
}
|
||||
}
|
||||
private async Task cerrarPopupModificacion()
|
||||
{
|
||||
await popupGestionDatos.HideAsync();
|
||||
}
|
||||
private async Task GestionarDatos(bool tipo)
|
||||
{
|
||||
var inci = ItemEnEdicion;
|
||||
if (tipo == true)
|
||||
{
|
||||
inci.IDDIFERENCIAPAGODELEGADO = 0;
|
||||
}
|
||||
inci.IDPERSONA = Persona.IDPERSONA;
|
||||
|
||||
if (inci.IDDIFERENCIAPAGODELEGADO != 0)
|
||||
{
|
||||
var response = await Utilidades.ActualizarObjeto(cliente, "/api/DIFERENCIAPAGODELEGADO/" + inci.IDDIFERENCIAPAGODELEGADO, inci);
|
||||
}
|
||||
else
|
||||
{
|
||||
var response = await Utilidades.NuevoObjeto(cliente, "/api/DIFERENCIAPAGODELEGADO/", inci);
|
||||
}
|
||||
await cerrarPopupModificacion();
|
||||
var response1 = await cliente.GetAsync($"/api/PERSONAS/PersonaNominaNif/{Persona.NIF}");
|
||||
if (!response1.IsSuccessStatusCode)
|
||||
{
|
||||
throw new Exception($"Error al obtener los datos de la persona. Código: {response1.StatusCode}");
|
||||
}
|
||||
|
||||
var resultContent = await response1.Content.ReadAsStringAsync();
|
||||
Persona = JsonConvert.DeserializeObject<PERSONAS>(resultContent) ?? throw new Exception("Error al deserializar los datos de la persona.");
|
||||
await CargarListas();
|
||||
|
||||
|
||||
}
|
||||
private async Task abrirPopupModificacion(DIFERENCIAPAGODELEGADO objeto, bool esNuevo)
|
||||
{
|
||||
ItemEnEdicion = objeto;
|
||||
await popupGestionDatos.ShowAsync();
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
@inject UserState UserState
|
||||
|
||||
<div class="tablaTabLateral">
|
||||
<input type="button" value="Nueva Huelga" @onclick="@(() => abrirPopupModificacion(new HUELGAS(), true))" class="mb-2 btnOAAFBlack" />
|
||||
<div style="overflow-x:auto;" class="tablaDesk">
|
||||
<Grid TItem="HUELGAS"
|
||||
Class="table tablaRegPers"
|
||||
@@ -65,23 +66,158 @@
|
||||
</Grid>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<Modal @ref="popupGestionDatos" IsVerticallyCentered="true" UseStaticBackdrop="true" CloseOnEscape="false">
|
||||
<BodyTemplate>
|
||||
<div class="row">
|
||||
|
||||
<div class="col-md-6 mb-2">
|
||||
<label class="fw-bold">Fecha inicio</label>
|
||||
<input class="form-control" type="date" @bind-value="ItemEnEdicion.FECHAINICIO" />
|
||||
</div>
|
||||
<div class="col-md-6 mb-2">
|
||||
<label class="fw-bold">Fecha fin</label>
|
||||
<input class="form-control" type="date" @bind-value="ItemEnEdicion.FECHAFIN" />
|
||||
</div>
|
||||
<div class="col-md-12 mb-2">
|
||||
<label for="txtEDesc" class="fw-bold">Nómina Origen de Datos: </label>
|
||||
<input list="listOD" id="selCon" @bind-value="@itmNomOD" type="text" style="width:100%" class="form-control" placeholder="Nómina Origen de Datos" />
|
||||
<datalist id="listOD">
|
||||
@foreach (NOMINAS con in lNominas)
|
||||
{
|
||||
<option data-value="@con.IDNOMINAS">@con.DESCRIPCION</option>
|
||||
}
|
||||
</datalist>
|
||||
</div>
|
||||
<div class="col-md-12 mb-2">
|
||||
<label for="txtEDesc" class="fw-bold">Nómina Aplicación: </label>
|
||||
<input list="listCon" id="selCon" @bind-value="@itmNomAp" type="text" style="width:100%" class="form-control" placeholder="Nómina Aplicación" />
|
||||
<datalist id="listCon">
|
||||
@foreach (NOMINAS con in lNominas)
|
||||
{
|
||||
<option data-value="@con.IDNOMINAS">@con.DESCRIPCION</option>
|
||||
}
|
||||
</datalist>
|
||||
</div>
|
||||
<div class="col-md-12 mb-2">
|
||||
|
||||
<label for="txtEDesc" class="fw-bold">Observaciones: </label>
|
||||
<input class="form-control" type="text" @bind-value="ItemEnEdicion.OBSERVACIONES" />
|
||||
</div>
|
||||
<div class="col-md-12" style="display:flex; justify-content:space-between">
|
||||
<label for="txtEDesc" class="fw-bold">Nomina normal: </label>
|
||||
<input class="" type="checkbox" id="chbNominaNormal" checked="@ItemEnEdicion.NOMINANORMAL" />
|
||||
|
||||
<label for="txtEDesc" class="fw-bold">Nomina seguridad social: </label>
|
||||
<input class="" type="checkbox" id="chbNominaSS" checked="@ItemEnEdicion.NOMINASEGURIDADSOCIAL" />
|
||||
</div>
|
||||
</div>
|
||||
</BodyTemplate>
|
||||
<FooterTemplate>
|
||||
<Button Color="ButtonColor.Secondary" @onclick="cerrarPopupModificacion">Cerrar</Button>
|
||||
@if (ItemEnEdicion.IDHUELGA != 0)
|
||||
{
|
||||
<Button Type="ButtonType.Submit" Color="ButtonColor.Primary" @onclick="@(() => GestionarDatos(false))">Modificar</Button>
|
||||
}
|
||||
else
|
||||
{
|
||||
<Button Type="ButtonType.Submit" Color="ButtonColor.Primary" @onclick="@(() => GestionarDatos(true))">Crear</Button>
|
||||
}
|
||||
</FooterTemplate>
|
||||
</Modal>
|
||||
|
||||
@code {
|
||||
[Parameter]
|
||||
public PERSONAS Persona { get; set; } = new PERSONAS();
|
||||
private Modal popupGestionDatos = default;
|
||||
private HttpClient cliente = new HttpClient();
|
||||
private string itmNomOD { get; set; }
|
||||
private string itmNomAp { get; set; }
|
||||
[Parameter]
|
||||
public EventCallback OnPersonaActualizada { get; set; }
|
||||
private List<HUELGAS> itmList = new List<HUELGAS>();
|
||||
private List<NOMINAS> lNominas = new List<NOMINAS>();
|
||||
private HUELGAS ItemEnEdicion { get; set; } = new HUELGAS();
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
cliente = Utilidades.ObtenerCliente(UserState.Token, HttpClientFactory);
|
||||
await CargarListas();
|
||||
}
|
||||
|
||||
private async Task CargarListas()
|
||||
{
|
||||
itmList.Clear();
|
||||
try
|
||||
{
|
||||
var listnom = Persona.HUELGAS;
|
||||
foreach (HUELGAS i in listnom) { itmList.Add(i); }
|
||||
var lNomi = await Utilidades.ObtenerObjeto<List<NOMINAS>>(cliente, "/api/NOMINAS");
|
||||
lNominas = lNomi.OrderBy(x => x.FECHAINICIO).ToList();
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Console.WriteLine($"Error al obtener los datos: {e.Message}");
|
||||
}
|
||||
}
|
||||
private async Task GestionarDatos(bool tipo)
|
||||
{
|
||||
var inci = ItemEnEdicion;
|
||||
inci.IDNOMINAAPLICACION = lNominas.FirstOrDefault(x => x.DESCRIPCION == itmNomAp).IDNOMINAS;
|
||||
inci.IDNOMINAORIGENDATOS = lNominas.FirstOrDefault(x => x.DESCRIPCION == itmNomOD).IDNOMINAS;
|
||||
if (tipo == true)
|
||||
{
|
||||
inci.IDHUELGA = 0;
|
||||
}
|
||||
inci.IDPERSONA = Persona.IDPERSONA;
|
||||
|
||||
string chbNominaNormal = "chbNominaNormal";
|
||||
inci.NOMINANORMAL = await JS.InvokeAsync<bool>("obtenerCheck", chbNominaNormal);
|
||||
string chbNominaSS = "chbNominaSS";
|
||||
inci.NOMINASEGURIDADSOCIAL = await JS.InvokeAsync<bool>("obtenerCheck", chbNominaSS);
|
||||
|
||||
|
||||
|
||||
if (inci.IDHUELGA != 0)
|
||||
{
|
||||
var response = await Utilidades.ActualizarObjeto(cliente, "/api/HUELGAS/" + inci.IDHUELGA, inci);
|
||||
}
|
||||
else
|
||||
{
|
||||
var response = await Utilidades.NuevoObjeto(cliente, "/api/HUELGAS/", inci);
|
||||
}
|
||||
await cerrarPopupModificacion();
|
||||
var response1 = await cliente.GetAsync($"/api/PERSONAS/PersonaNominaNif/{Persona.NIF}");
|
||||
if (!response1.IsSuccessStatusCode)
|
||||
{
|
||||
throw new Exception($"Error al obtener los datos de la persona. Código: {response1.StatusCode}");
|
||||
}
|
||||
|
||||
var resultContent = await response1.Content.ReadAsStringAsync();
|
||||
Persona = JsonConvert.DeserializeObject<PERSONAS>(resultContent) ?? throw new Exception("Error al deserializar los datos de la persona.");
|
||||
await CargarListas();
|
||||
|
||||
|
||||
}
|
||||
private async Task abrirPopupModificacion(HUELGAS objeto, bool esNuevo)
|
||||
{
|
||||
if (objeto.IDHUELGA != 0)
|
||||
{
|
||||
itmNomOD = objeto.IDNOMINAORIGENDATOSNavigation.DESCRIPCION;
|
||||
itmNomAp = objeto.IDNOMINAAPLICACIONNavigation?.DESCRIPCION;
|
||||
}
|
||||
else
|
||||
{
|
||||
itmNomAp = "";
|
||||
itmNomOD = "";
|
||||
}
|
||||
|
||||
ItemEnEdicion = objeto;
|
||||
await popupGestionDatos.ShowAsync();
|
||||
}
|
||||
private async Task cerrarPopupModificacion()
|
||||
{
|
||||
await popupGestionDatos.HideAsync();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
@inject UserState UserState
|
||||
|
||||
<div class="tablaTabLateral">
|
||||
<input type="button" value="Nuevo Permiso sin retribución" @onclick="@(() => abrirPopupModificacion(new PERMISOSSINRETRIBUCION(), true))" class="mb-2 btnOAAFBlack" />
|
||||
<div style="overflow-x:auto;" class="tablaDesk">
|
||||
<Grid TItem="PERMISOSSINRETRIBUCION"
|
||||
Class="table tablaRegPers"
|
||||
@@ -31,6 +32,9 @@
|
||||
PaginationItemsTextFormat="{0} - {1} de {2} elementos">
|
||||
|
||||
<GridColumns>
|
||||
<GridColumn TItem="PERMISOSSINRETRIBUCION" HeaderText="">
|
||||
<button @onclick="@(() => abrirPopupModificacion(context, false))" class="btnOAAFAzul">Editar</button>
|
||||
</GridColumn>
|
||||
<GridColumn TItem="PERMISOSSINRETRIBUCION" HeaderText="Fecha Inicio">
|
||||
@context.FECHAINICIO?.ToString("dd/MM/yyyy")
|
||||
</GridColumn>
|
||||
@@ -62,24 +66,162 @@
|
||||
</Grid>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Modal @ref="popupGestionDatos" IsVerticallyCentered="true" UseStaticBackdrop="true" CloseOnEscape="false">
|
||||
<BodyTemplate>
|
||||
<div class="row">
|
||||
|
||||
<div class="col-md-6 mb-2">
|
||||
<label class="fw-bold">Fecha inicio</label>
|
||||
<input class="form-control" type="date" @bind-value="ItemEnEdicion.FECHAINICIO" />
|
||||
</div>
|
||||
<div class="col-md-6 mb-2">
|
||||
<label class="fw-bold">Fecha fin</label>
|
||||
<input class="form-control" type="date" @bind-value="ItemEnEdicion.FECHAFIN" />
|
||||
</div>
|
||||
<div class="col-md-12 mb-2">
|
||||
<label for="txtEDesc" class="fw-bold">Nómina Origen de Datos: </label>
|
||||
<input list="listOD" id="selCon" @bind-value="@itmNomOD" type="text" style="width:100%" class="form-control" placeholder="Nómina Origen de Datos" />
|
||||
<datalist id="listOD">
|
||||
@foreach (NOMINAS con in lNominas)
|
||||
{
|
||||
<option data-value="@con.IDNOMINAS">@con.DESCRIPCION</option>
|
||||
}
|
||||
</datalist>
|
||||
</div>
|
||||
<div class="col-md-12 mb-2">
|
||||
<label for="txtEDesc" class="fw-bold">Nómina Aplicación: </label>
|
||||
<input list="listCon" id="selCon" @bind-value="@itmNomAp" type="text" style="width:100%" class="form-control" placeholder="Nómina Aplicación" />
|
||||
<datalist id="listCon">
|
||||
@foreach (NOMINAS con in lNominas)
|
||||
{
|
||||
<option data-value="@con.IDNOMINAS">@con.DESCRIPCION</option>
|
||||
}
|
||||
</datalist>
|
||||
</div>
|
||||
<div class="col-md-12 mb-2">
|
||||
|
||||
<label for="txtEDesc" class="fw-bold">Observaciones: </label>
|
||||
<input class="form-control" type="text" @bind-value="ItemEnEdicion.OBSERVACIONES" />
|
||||
</div>
|
||||
<div class="col-md-12" style="display:flex; justify-content:space-between">
|
||||
<label for="txtEDesc" class="fw-bold">Nomina normal: </label>
|
||||
<input class="" type="checkbox" id="chbNominaNormal" checked="@ItemEnEdicion.NOMINANORMAL" />
|
||||
|
||||
<label for="txtEDesc" class="fw-bold">Nomina seguridad social: </label>
|
||||
<input class="" type="checkbox" id="chbNominaSS" checked="@ItemEnEdicion.NOMINASEGURIDADSOCIAL" />
|
||||
</div>
|
||||
</div>
|
||||
</BodyTemplate>
|
||||
<FooterTemplate>
|
||||
<Button Color="ButtonColor.Secondary" @onclick="cerrarPopupModificacion">Cerrar</Button>
|
||||
@if (ItemEnEdicion.IDPERMISOSDERETRIBUCION != 0)
|
||||
{
|
||||
<Button Type="ButtonType.Submit" Color="ButtonColor.Primary" @onclick="@(() => GestionarDatos(false))">Modificar</Button>
|
||||
}
|
||||
else
|
||||
{
|
||||
<Button Type="ButtonType.Submit" Color="ButtonColor.Primary" @onclick="@(() => GestionarDatos(true))">Crear</Button>
|
||||
}
|
||||
</FooterTemplate>
|
||||
</Modal>
|
||||
|
||||
|
||||
|
||||
@code {
|
||||
[Parameter]
|
||||
public PERSONAS Persona { get; set; } = new PERSONAS();
|
||||
private HttpClient cliente = new HttpClient();
|
||||
private Modal popupGestionDatos = default;
|
||||
private string itmNomOD { get; set; }
|
||||
private string itmNomAp { get; set; }
|
||||
[Parameter]
|
||||
public EventCallback OnPersonaActualizada { get; set; }
|
||||
private PERMISOSSINRETRIBUCION ItemEnEdicion { get; set; } = new PERMISOSSINRETRIBUCION();
|
||||
private List<PERMISOSSINRETRIBUCION> itmList = new List<PERMISOSSINRETRIBUCION>();
|
||||
private List<NOMINAS> lNominas = new List<NOMINAS>();
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
cliente = Utilidades.ObtenerCliente(UserState.Token, HttpClientFactory);
|
||||
await CargarListas();
|
||||
}
|
||||
|
||||
private async Task CargarListas()
|
||||
{
|
||||
itmList.Clear();
|
||||
try
|
||||
{
|
||||
var listnom = Persona.PERMISOSSINRETRIBUCION;
|
||||
foreach (PERMISOSSINRETRIBUCION i in listnom) { itmList.Add(i); }
|
||||
var lNomi = await Utilidades.ObtenerObjeto<List<NOMINAS>>(cliente, "/api/NOMINAS");
|
||||
lNominas = lNomi.OrderBy(x => x.FECHAINICIO).ToList();
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Console.WriteLine($"Error al obtener los datos: {e.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private async Task GestionarDatos(bool tipo)
|
||||
{
|
||||
var inci = ItemEnEdicion;
|
||||
inci.IDNOMINAAPLICACION = lNominas.FirstOrDefault(x => x.DESCRIPCION == itmNomAp).IDNOMINAS;
|
||||
inci.IDNOMINAORIGENDEDATOS = lNominas.FirstOrDefault(x => x.DESCRIPCION == itmNomOD).IDNOMINAS;
|
||||
if (tipo == true)
|
||||
{
|
||||
inci.IDPERMISOSDERETRIBUCION = 0;
|
||||
}
|
||||
inci.IDPERSONA = Persona.IDPERSONA;
|
||||
|
||||
string chbNominaNormal = "chbNominaNormal";
|
||||
inci.NOMINANORMAL = await JS.InvokeAsync<bool>("obtenerCheck", chbNominaNormal);
|
||||
string chbNominaSS = "chbNominaSS";
|
||||
inci.NOMINASEGURIDADSOCIAL = await JS.InvokeAsync<bool>("obtenerCheck", chbNominaSS);
|
||||
|
||||
|
||||
|
||||
if (inci.IDPERMISOSDERETRIBUCION != 0)
|
||||
{
|
||||
var response = await Utilidades.ActualizarObjeto(cliente, "/api/PERMISOSSINRETRIBUCION/" + inci.IDPERMISOSDERETRIBUCION, inci);
|
||||
}
|
||||
else
|
||||
{
|
||||
var response = await Utilidades.NuevoObjeto(cliente, "/api/PERMISOSSINRETRIBUCION/", inci);
|
||||
}
|
||||
await cerrarPopupModificacion();
|
||||
var response1 = await cliente.GetAsync($"/api/PERSONAS/PersonaNominaNif/{Persona.NIF}");
|
||||
if (!response1.IsSuccessStatusCode)
|
||||
{
|
||||
throw new Exception($"Error al obtener los datos de la persona. Código: {response1.StatusCode}");
|
||||
}
|
||||
|
||||
var resultContent = await response1.Content.ReadAsStringAsync();
|
||||
Persona = JsonConvert.DeserializeObject<PERSONAS>(resultContent) ?? throw new Exception("Error al deserializar los datos de la persona.");
|
||||
await CargarListas();
|
||||
|
||||
|
||||
}
|
||||
private async Task abrirPopupModificacion(PERMISOSSINRETRIBUCION objeto, bool esNuevo)
|
||||
{
|
||||
if(objeto.IDPERMISOSDERETRIBUCION != 0)
|
||||
{
|
||||
itmNomOD = objeto.IDNOMINAORIGENDEDATOSNavigation?.DESCRIPCION;
|
||||
itmNomAp = objeto.IDNOMINAAPLICACIONNavigation?.DESCRIPCION;
|
||||
}
|
||||
else
|
||||
{
|
||||
itmNomAp = "";
|
||||
itmNomOD = "";
|
||||
}
|
||||
|
||||
ItemEnEdicion = objeto;
|
||||
await popupGestionDatos.ShowAsync();
|
||||
}
|
||||
private async Task cerrarPopupModificacion()
|
||||
{
|
||||
await popupGestionDatos.HideAsync();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
using PdfSharpCore.Drawing;
|
||||
@@ -10,14 +11,30 @@ namespace GestionaDenuncias.Shared.Helpers
|
||||
{
|
||||
public static class PdfHelper
|
||||
{
|
||||
private const double ReportTextMarginLeft = 95;
|
||||
private const double ReportPdfSignatureGutter = 70;
|
||||
private const double ReportPdfRightMargin = 20;
|
||||
|
||||
/// <summary>
|
||||
/// Fusiona varios ficheros (PDF, im<EFBFBD>genes, TXT) en un <EFBFBD>nico PDF.
|
||||
/// Los .txt se renderizan con m<EFBFBD>rgenes iguales, alineaci<EFBFBD>n a la izquierda y ajuste de l<EFBFBD>neas,
|
||||
/// preservando l<EFBFBD>neas en blanco.
|
||||
/// Fusiona varios ficheros (PDF, imágenes, TXT) en un único PDF.
|
||||
/// Los .txt se renderizan con márgenes iguales, alineación a la izquierda y ajuste de líneas,
|
||||
/// preservando líneas en blanco.
|
||||
/// </summary>
|
||||
/// <param name="files">Secuencia de tuplas (FileName, ContentBytes)</param>
|
||||
/// <returns>Bytes del PDF combinado</returns>
|
||||
public static byte[] MergeFilesToPdf(IEnumerable<(string FileName, byte[] Content)> files)
|
||||
=> MergeFilesToPdf(files, textMarginLeft: 40, insetPdfPagesForSignature: false);
|
||||
|
||||
public static byte[] MergeReportToPdf(IEnumerable<(string FileName, byte[] Content)> files)
|
||||
=> MergeFilesToPdf(
|
||||
files,
|
||||
textMarginLeft: ReportTextMarginLeft,
|
||||
insetPdfPagesForSignature: true);
|
||||
|
||||
private static byte[] MergeFilesToPdf(
|
||||
IEnumerable<(string FileName, byte[] Content)> files,
|
||||
double textMarginLeft,
|
||||
bool insetPdfPagesForSignature)
|
||||
{
|
||||
if (files == null) throw new ArgumentNullException(nameof(files));
|
||||
|
||||
@@ -33,8 +50,16 @@ namespace GestionaDenuncias.Shared.Helpers
|
||||
{
|
||||
case ".pdf":
|
||||
using (var src = PdfReader.Open(new MemoryStream(content), PdfDocumentOpenMode.Import))
|
||||
{
|
||||
foreach (var page in src.Pages)
|
||||
outputDoc.AddPage(page);
|
||||
{
|
||||
var importedPage = outputDoc.AddPage(page);
|
||||
if (insetPdfPagesForSignature)
|
||||
{
|
||||
InsetPdfPageForSignature(importedPage);
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case ".jpg":
|
||||
@@ -52,12 +77,12 @@ namespace GestionaDenuncias.Shared.Helpers
|
||||
break;
|
||||
|
||||
case ".txt":
|
||||
// Renderizado de TXT con margen y ajuste de l<EFBFBD>neas, preservando l<EFBFBD>neas en blanco
|
||||
// Renderizado de TXT con margen y ajuste de líneas, preservando líneas en blanco
|
||||
var text = Encoding.UTF8.GetString(content);
|
||||
PdfPage pageTxt = outputDoc.AddPage();
|
||||
XGraphics gfxTxt = XGraphics.FromPdfPage(pageTxt);
|
||||
|
||||
const double marginLeft = 40;
|
||||
var marginLeft = textMarginLeft;
|
||||
const double marginRight = 40;
|
||||
const double marginTop = 40;
|
||||
const double marginBottom = 40;
|
||||
@@ -72,7 +97,7 @@ namespace GestionaDenuncias.Shared.Helpers
|
||||
|
||||
foreach (var origLine in text.Replace("\r\n", "\n").Replace('\r', '\n').Split('\n'))
|
||||
{
|
||||
// L<EFBFBD>nea en blanco: preservarla
|
||||
// Línea en blanco: preservarla
|
||||
if (string.IsNullOrWhiteSpace(origLine))
|
||||
{
|
||||
y += lineHeight;
|
||||
@@ -99,7 +124,7 @@ namespace GestionaDenuncias.Shared.Helpers
|
||||
}
|
||||
else
|
||||
{
|
||||
// Dibujar la l<EFBFBD>nea acumulada
|
||||
// Dibujar la línea acumulada
|
||||
gfxTxt.DrawString(
|
||||
currentLine,
|
||||
font,
|
||||
@@ -109,7 +134,7 @@ namespace GestionaDenuncias.Shared.Helpers
|
||||
y += lineHeight;
|
||||
currentLine = word;
|
||||
|
||||
// Paginaci<EFBFBD>n si se sale por abajo
|
||||
// Paginación si se sale por abajo
|
||||
if (y + lineHeight > pageHeight - marginBottom)
|
||||
{
|
||||
gfxTxt.Dispose();
|
||||
@@ -120,7 +145,7 @@ namespace GestionaDenuncias.Shared.Helpers
|
||||
}
|
||||
}
|
||||
|
||||
// Dibujar la <EFBFBD>ltima l<EFBFBD>nea del p<EFBFBD>rrafo
|
||||
// Dibujar la última línea del párrafo
|
||||
if (!string.IsNullOrEmpty(currentLine))
|
||||
{
|
||||
gfxTxt.DrawString(
|
||||
@@ -145,7 +170,7 @@ namespace GestionaDenuncias.Shared.Helpers
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new NotSupportedException($"Extensi<EFBFBD>n no soportada: {ext}");
|
||||
throw new NotSupportedException($"Extensión no soportada: {ext}");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -154,6 +179,23 @@ namespace GestionaDenuncias.Shared.Helpers
|
||||
return ms.ToArray();
|
||||
}
|
||||
|
||||
private static void InsetPdfPageForSignature(PdfPage page)
|
||||
{
|
||||
var pageWidth = page.Width.Point;
|
||||
var availableWidth = Math.Max(1, pageWidth - ReportPdfSignatureGutter - ReportPdfRightMargin);
|
||||
var scaleX = Math.Min(1, availableWidth / pageWidth);
|
||||
|
||||
var command = string.Create(
|
||||
CultureInfo.InvariantCulture,
|
||||
$"q {scaleX:0.#####} 0 0 1 {ReportPdfSignatureGutter:0.#####} 0 cm\n");
|
||||
|
||||
var prepend = page.Contents.PrependContent();
|
||||
prepend.Stream = prepend.CreateStream(Encoding.ASCII.GetBytes(command));
|
||||
|
||||
var append = page.Contents.AppendContent();
|
||||
append.Stream = append.CreateStream(Encoding.ASCII.GetBytes("\nQ\n"));
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -19,7 +19,9 @@ public sealed record InboxSnapshotResponse(
|
||||
IReadOnlyList<ReportDto> Reports,
|
||||
InboxUserState UserState);
|
||||
|
||||
public sealed record ImportReportRequest(ReportDto Report);
|
||||
public sealed record ImportReportRequest(
|
||||
ReportDto Report,
|
||||
bool ConfirmDifferentOwner = false);
|
||||
|
||||
public sealed record MarkFicherosUploadedRequest(
|
||||
IReadOnlyList<string> FileNames,
|
||||
@@ -36,12 +38,17 @@ public sealed record MarkReportImportedRequest(
|
||||
ReportDto Report,
|
||||
int? ComplaintId);
|
||||
|
||||
public sealed record MarkReportHandledInGestionaRequest(
|
||||
string Username,
|
||||
int DenunciaId,
|
||||
DateTime UploadedAtUtc);
|
||||
|
||||
public sealed record TrackingImportPermissionRequest(
|
||||
string Username,
|
||||
ReportDto Report);
|
||||
ReportDto Report,
|
||||
bool ConfirmDifferentOwner = false);
|
||||
|
||||
public sealed record GestionaCreateFileRequest(
|
||||
Guid ProcedureId,
|
||||
string Subject,
|
||||
string DocumentSeries,
|
||||
string SiaCode);
|
||||
@@ -53,16 +60,22 @@ public sealed record GestionaCreateFileResponse(
|
||||
public sealed record GestionaOpenFileRequest(
|
||||
string FileUrl,
|
||||
string? FileOpenUrl,
|
||||
Guid ManagementUnitGroupId,
|
||||
Guid AssignedGroupId,
|
||||
string AssignedGroupCode,
|
||||
bool Confidential,
|
||||
string FreeTitle,
|
||||
string SiaCode);
|
||||
string FreeTitle);
|
||||
|
||||
public sealed record GestionaAssignFileRequest(
|
||||
string FileUrl,
|
||||
string AssignedGroupCode);
|
||||
|
||||
public sealed record GestionaEnsureThirdRequest(
|
||||
string FileUrl,
|
||||
ThirdPartyIdentityData ThirdParty);
|
||||
|
||||
public sealed record GestionaEnsureThirdResponse(
|
||||
bool Ok,
|
||||
IReadOnlyList<string> Warnings);
|
||||
|
||||
public sealed record GestionaCreateFolderRequest(
|
||||
string FileUrl,
|
||||
string FolderName);
|
||||
@@ -80,8 +93,10 @@ public sealed record GestionaUploadDocumentResponse(string DocumentUrl);
|
||||
|
||||
public sealed record GestionaTramitarDocumentoRequest(
|
||||
string DocumentUrl,
|
||||
string AssignedGroupHref,
|
||||
int? ComplaintId);
|
||||
string AssignedGroupCode,
|
||||
int? ComplaintId,
|
||||
bool IsUpdate = false,
|
||||
string? UpdateSource = null);
|
||||
|
||||
public sealed record ManualPurgeRequest(string Date);
|
||||
|
||||
@@ -91,6 +106,50 @@ public sealed record ManualPurgeResponse(
|
||||
int StatusCode,
|
||||
string ResponseBody);
|
||||
|
||||
public sealed record AppConfigurationDto(string? ExternalUpdateCutoffDate);
|
||||
public sealed record AppConfigurationDto(
|
||||
string? ExternalUpdateCutoffDate,
|
||||
string? LatestEncryptionKeyDate = null,
|
||||
string? LatestEncryptionKeyStatus = null);
|
||||
|
||||
public sealed record UpdateExternalUpdateCutoffRequest(string? Date);
|
||||
|
||||
public sealed record WorkGroupDto(
|
||||
string Code,
|
||||
string Name);
|
||||
|
||||
public sealed record UserWorkGroupDto(
|
||||
long UserId,
|
||||
string Username,
|
||||
IReadOnlyList<string> GroupCodes);
|
||||
|
||||
public sealed record WorkGroupAdministrationDto(
|
||||
IReadOnlyList<WorkGroupDto> Groups,
|
||||
IReadOnlyList<UserWorkGroupDto> Users);
|
||||
|
||||
public sealed record CurrentUserWorkGroupsDto(
|
||||
string Username,
|
||||
IReadOnlyList<string> GroupCodes);
|
||||
|
||||
public sealed record UpdateUserWorkGroupsRequest(
|
||||
IReadOnlyList<string> GroupCodes);
|
||||
|
||||
public sealed record GestionaComplaintFieldsResponse(
|
||||
DateTime? FechaDenuncia,
|
||||
int NumeroDenunciaCanal,
|
||||
string AQuienDenuncia,
|
||||
string ResumenDenuncia,
|
||||
string FechaHechos,
|
||||
string LugarHechos,
|
||||
string AmbitoCompetencias,
|
||||
string SolicitaProteccion,
|
||||
string SexoDenunciante,
|
||||
string AutorizaRemisionDenuncia,
|
||||
string AutorizaNotificacionesViaSms,
|
||||
string PreferenciaNotificacionSeguimientoDenuncia);
|
||||
|
||||
public sealed record GestionaExternalFieldValue(
|
||||
string Type,
|
||||
string Value);
|
||||
|
||||
public sealed record GestionaExternalFieldsResponse(
|
||||
IReadOnlyDictionary<string, GestionaExternalFieldValue> Data);
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
namespace GestionaDenuncias.Shared.Models;
|
||||
|
||||
public static class ComplaintUpdateSources
|
||||
{
|
||||
public const string Citizen = "citizen";
|
||||
public const string Receiver = "receiver";
|
||||
|
||||
public static bool IsCitizen(string? value)
|
||||
=> string.Equals(value, Citizen, StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
public static bool IsReceiver(string? value)
|
||||
=> string.Equals(value, Receiver, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
@@ -77,10 +77,18 @@ public class DenunciasGestiona
|
||||
public string EstadoDenuncia { get; set; } = string.Empty;
|
||||
public string ArchivoElegido { get; set; } = string.Empty;
|
||||
public DateTime FechaSubidaAGestiona { get; set; } = DateTime.MinValue;
|
||||
public string UltimaSubidaGestionaTipo { get; set; } = string.Empty;
|
||||
public string UltimoGrupoAsignadoGestiona { get; set; } = string.Empty;
|
||||
public string PendingUpdateSource { get; set; } = string.Empty;
|
||||
|
||||
public bool EnGestiona { get; set; }
|
||||
public bool EnRechazada { get; set; }
|
||||
|
||||
public string OwnerUsername { get; set; } = string.Empty;
|
||||
public bool OwnedByCurrentUser { get; set; }
|
||||
public bool RequiresOwnerConfirmation { get; set; }
|
||||
public IReadOnlyList<string> OwnerWorkGroups { get; set; } = [];
|
||||
|
||||
[JsonIgnore]
|
||||
public DateOnly? KeyDate { get; set; }
|
||||
|
||||
@@ -185,6 +193,18 @@ public class DenunciasGestiona
|
||||
}
|
||||
}
|
||||
|
||||
[JsonIgnore]
|
||||
public string UltimaSubidaGestionaTipoMostrable =>
|
||||
string.IsNullOrWhiteSpace(UltimaSubidaGestionaTipo)
|
||||
? string.Empty
|
||||
: UltimaSubidaGestionaTipo.Trim();
|
||||
|
||||
[JsonIgnore]
|
||||
public string UltimoGrupoAsignadoGestionaMostrable =>
|
||||
string.IsNullOrWhiteSpace(UltimoGrupoAsignadoGestiona)
|
||||
? string.Empty
|
||||
: UltimoGrupoAsignadoGestiona.Trim();
|
||||
|
||||
public IReadOnlyList<ReportFieldEntry> GetCamposFormulario()
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(CamposFormularioJson))
|
||||
|
||||
@@ -5,7 +5,10 @@ namespace GestionaDenuncias.Shared.Models
|
||||
public string FileUrl { get; set; } = "";
|
||||
public string? CodigoExpediente { get; set; } // name / code / etc.
|
||||
public string? Asunto { get; set; } // subject / free_title
|
||||
public string? Procedimiento { get; set; }
|
||||
public DateTimeOffset? FechaCreacion { get; set; }
|
||||
public DateTimeOffset? FechaUltimaModificacion { get; set; }
|
||||
public string? UltimaAuditoriaMensaje { get; set; }
|
||||
public string? Estado { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
namespace GestionaDenuncias.Shared.Models;
|
||||
|
||||
public sealed class GestionaAuditInfo
|
||||
{
|
||||
public DateTimeOffset? Fecha { get; set; }
|
||||
public string? Mensaje { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
namespace GestionaDenuncias.Shared.Models;
|
||||
|
||||
public sealed record GestionaUploadHistoryEntry(
|
||||
long Id,
|
||||
int DenunciaId,
|
||||
string ExpedienteGestionaUrl,
|
||||
string CodigoExpedienteGestiona,
|
||||
string TipoSubida,
|
||||
string GrupoAsignado,
|
||||
string Usuario,
|
||||
DateTime UploadedAtUtc,
|
||||
string Asunto,
|
||||
string Documentos);
|
||||
|
||||
public sealed record GestionaUploadHistoryCreateRequest(
|
||||
int DenunciaId,
|
||||
string ExpedienteGestionaUrl,
|
||||
string CodigoExpedienteGestiona,
|
||||
string TipoSubida,
|
||||
string GrupoAsignado,
|
||||
DateTime UploadedAtUtc,
|
||||
string Asunto,
|
||||
string Documentos);
|
||||
@@ -1,3 +1,11 @@
|
||||
namespace GestionaDenuncias.Shared.Models;
|
||||
|
||||
public sealed record GlSession(string Id, string Username, string? Role = null);
|
||||
public sealed record GlobalLeaksProofOfWorkToken(string Id, string Salt);
|
||||
|
||||
public sealed record GlSession(
|
||||
string Id,
|
||||
string Username,
|
||||
string? Role = null,
|
||||
string? DpopPrivateKey = null,
|
||||
GlobalLeaksProofOfWorkToken? ProofOfWorkToken = null,
|
||||
DateTimeOffset? SessionExpiresAtUtc = null);
|
||||
|
||||
@@ -6,7 +6,15 @@ public sealed class GlobalLeaksStoredSession
|
||||
public string Password { get; set; } = string.Empty;
|
||||
public string? SessionId { get; set; }
|
||||
public string? Role { get; set; }
|
||||
public string? DpopPrivateKey { get; set; }
|
||||
public GlobalLeaksProofOfWorkToken? ProofOfWorkToken { get; set; }
|
||||
public DateTimeOffset? SessionExpiresAtUtc { get; set; }
|
||||
public DateTimeOffset? LastKeepAliveAtUtc { get; set; }
|
||||
public DateTimeOffset UpdatedAt { get; set; } = DateTimeOffset.UtcNow;
|
||||
|
||||
public bool HasActiveSession => !string.IsNullOrWhiteSpace(SessionId);
|
||||
public bool HasActiveSession =>
|
||||
!string.IsNullOrWhiteSpace(SessionId) &&
|
||||
!string.IsNullOrWhiteSpace(DpopPrivateKey) &&
|
||||
!string.IsNullOrWhiteSpace(ProofOfWorkToken?.Id) &&
|
||||
!string.IsNullOrWhiteSpace(ProofOfWorkToken?.Salt);
|
||||
}
|
||||
|
||||
@@ -5,14 +5,17 @@ public sealed record ReportDetailDto(
|
||||
string? LastAccess,
|
||||
IReadOnlyList<ReportCommentDto> Comments,
|
||||
IReadOnlyList<ReportFileDto> WhistleblowerFiles,
|
||||
IReadOnlyList<ReportFileDto> ReceiverFiles);
|
||||
IReadOnlyList<ReportFileDto> ReceiverFiles,
|
||||
IReadOnlyList<ReportReceiverDto>? Receivers = null);
|
||||
|
||||
public sealed record ReportCommentDto(
|
||||
string? Id,
|
||||
string? Type,
|
||||
string? Content,
|
||||
string? CreationDate,
|
||||
bool IsNew);
|
||||
bool IsNew,
|
||||
string? AuthorId = null,
|
||||
string? AuthorName = null);
|
||||
|
||||
public sealed record ReportFileDto(
|
||||
string? Id,
|
||||
@@ -20,4 +23,11 @@ public sealed record ReportFileDto(
|
||||
long? Size,
|
||||
string? ContentType,
|
||||
string? CreationDate,
|
||||
bool IsNew);
|
||||
bool IsNew,
|
||||
string? AuthorId = null,
|
||||
string? AuthorName = null);
|
||||
|
||||
public sealed record ReportReceiverDto(
|
||||
string Id,
|
||||
string Name,
|
||||
bool Active);
|
||||
|
||||
@@ -12,14 +12,31 @@ public sealed record ReportDto
|
||||
public string? ReminderDate { get; init; }
|
||||
public string? AccessDate { get; init; }
|
||||
public string? LastAccess { get; init; }
|
||||
public string? WhistleblowerLastAccess { get; init; }
|
||||
public string? Status { get; init; }
|
||||
public bool Updated { get; init; }
|
||||
public bool? Accessible { get; init; }
|
||||
public string? Label { get; init; }
|
||||
public bool ActivityAnalyzed { get; init; }
|
||||
public string? CitizenLastActivity { get; init; }
|
||||
public bool CitizenHasNewActivity { get; init; }
|
||||
public bool CitizenHasNewComment { get; init; }
|
||||
public bool CitizenHasNewFile { get; init; }
|
||||
public string? ReceiverLastActivity { get; init; }
|
||||
public bool ReceiverHasNewActivity { get; init; }
|
||||
public string? ReceiverLastActivityAuthorId { get; init; }
|
||||
public string? ReceiverLastActivityAuthorName { get; init; }
|
||||
public bool DownloadedByCurrentUser { get; init; }
|
||||
public bool DownloadedByAnotherUser { get; init; }
|
||||
public string? LastDownloadedByUsername { get; init; }
|
||||
public string? LastDownloadedAt { get; init; }
|
||||
public string? LastGestionaUploadAt { get; init; }
|
||||
public bool AlreadyImported { get; init; }
|
||||
public bool AlreadyInGestiona { get; init; }
|
||||
public string? OwnerUsername { get; init; }
|
||||
public bool OwnedByCurrentUser { get; init; }
|
||||
public bool AccessibleByWorkGroup { get; init; }
|
||||
public bool RequiresOwnerConfirmation { get; init; }
|
||||
public IReadOnlyList<string> OwnerWorkGroups { get; init; } = [];
|
||||
public string? TrackingNote { get; init; }
|
||||
}
|
||||
|
||||
@@ -12,6 +12,9 @@ public sealed class ThirdPartyIdentityData
|
||||
public string BusinessName { get; set; } = string.Empty;
|
||||
public string Email { get; set; } = string.Empty;
|
||||
public string CountryCode { get; set; } = string.Empty;
|
||||
public string NotificationPreference { get; set; } = string.Empty;
|
||||
public string ElectronicNotification { get; set; } = string.Empty;
|
||||
public string PostalNotificationPreference { get; set; } = string.Empty;
|
||||
public ThirdPartyAddressData? Address { get; set; }
|
||||
|
||||
public string DisplayName =>
|
||||
@@ -53,6 +56,9 @@ public sealed class ThirdPartyIdentityData
|
||||
BusinessName = isAnonymous ? string.Empty : businessName,
|
||||
Email = (denuncia.Correo_Electronico ?? string.Empty).Trim(),
|
||||
CountryCode = string.IsNullOrWhiteSpace(denuncia.Pais) ? denuncia.PaisOrigen : denuncia.Pais,
|
||||
NotificationPreference = denuncia.Notificacion_Preferencia ?? string.Empty,
|
||||
ElectronicNotification = denuncia.Notificacion_Electronica ?? string.Empty,
|
||||
PostalNotificationPreference = denuncia.NotificacionPostal ?? string.Empty,
|
||||
Address = isAnonymous ? null : ThirdPartyAddressData.FromComplaint(denuncia)
|
||||
};
|
||||
}
|
||||
|
||||
@@ -9,9 +9,14 @@ public interface IDenunciaStore
|
||||
Task<List<DenunciasGestiona>> GetDenunciasByScopeAsync(DenunciaListScope scope, CancellationToken cancellationToken = default);
|
||||
Task<List<FicherosDenuncias>> GetAllFicherosAsync(CancellationToken cancellationToken = default);
|
||||
Task<List<FicherosDenuncias>> GetFicherosByDenunciaAsync(int denunciaId, CancellationToken cancellationToken = default);
|
||||
Task<List<GestionaUploadHistoryEntry>> GetGestionaUploadHistoryAsync(CancellationToken cancellationToken = default);
|
||||
Task<DenunciasGestiona?> GetDenunciaByIdAsync(int denunciaId, CancellationToken cancellationToken = default);
|
||||
Task UpsertDenunciaAsync(DenunciasGestiona denuncia, CancellationToken cancellationToken = default);
|
||||
Task UpsertFicherosAsync(IEnumerable<FicherosDenuncias> ficheros, CancellationToken cancellationToken = default);
|
||||
Task AddGestionaUploadHistoryAsync(
|
||||
GestionaUploadHistoryCreateRequest request,
|
||||
string username,
|
||||
CancellationToken cancellationToken = default);
|
||||
Task MarkFicherosAsUploadedAsync(
|
||||
int denunciaId,
|
||||
IEnumerable<string> fileNames,
|
||||
|
||||
@@ -15,8 +15,15 @@ public interface IInboxTrackingService
|
||||
int? complaintId,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
Task MarkReportHandledInGestionaAsync(
|
||||
string username,
|
||||
int denunciaId,
|
||||
DateTime uploadedAtUtc,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
Task EnsureReportCanBeImportedByUserAsync(
|
||||
string username,
|
||||
ReportDto report,
|
||||
bool confirmDifferentOwner = false,
|
||||
CancellationToken cancellationToken = default);
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
<body>
|
||||
<Routes @rendermode="@(new InteractiveServerRenderMode(prerender: false))" />
|
||||
<script src="Scripts/bootstrap.bundle.min.js"></script>
|
||||
<script src="js/appAuth.js"></script>
|
||||
<script src="js/appAuth.js?v=20260724-session-keepalive"></script>
|
||||
|
||||
<script src="_framework/blazor.web.js"></script>
|
||||
</body>
|
||||
|
||||
@@ -0,0 +1,110 @@
|
||||
@implements IDisposable
|
||||
@inject UiDialogService Dialog
|
||||
|
||||
@if (Dialog.IsVisible)
|
||||
{
|
||||
<div class="app-confirmation-backdrop"
|
||||
role="presentation"
|
||||
@onkeydown="HandleKeyDown">
|
||||
<section class="@DialogCss"
|
||||
role="alertdialog"
|
||||
aria-modal="true"
|
||||
aria-labelledby="app-confirmation-title"
|
||||
aria-describedby="app-confirmation-message">
|
||||
<div class="app-confirmation__accent" aria-hidden="true"></div>
|
||||
|
||||
<div class="app-confirmation__content">
|
||||
<div class="@IconCss" aria-hidden="true">!</div>
|
||||
|
||||
<div class="app-confirmation__copy">
|
||||
<div class="app-confirmation__eyebrow">@Eyebrow</div>
|
||||
<h2 id="app-confirmation-title" class="app-confirmation__title">
|
||||
@Dialog.Title
|
||||
</h2>
|
||||
<p id="app-confirmation-message" class="app-confirmation__message">
|
||||
@Dialog.Message
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="app-confirmation__actions">
|
||||
<button type="button"
|
||||
class="btn btn-outline-secondary app-confirmation__button"
|
||||
@ref="_cancelButton"
|
||||
@onclick="Dialog.Cancel">
|
||||
@Dialog.CancelText
|
||||
</button>
|
||||
<button type="button"
|
||||
class="@ConfirmButtonCss"
|
||||
@onclick="Dialog.Confirm">
|
||||
@Dialog.ConfirmText
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
}
|
||||
|
||||
@code {
|
||||
private ElementReference _cancelButton;
|
||||
private bool _focusPending;
|
||||
|
||||
private string DialogCss =>
|
||||
$"app-confirmation app-confirmation--{ToneName}";
|
||||
|
||||
private string IconCss =>
|
||||
$"app-confirmation__icon app-confirmation__icon--{ToneName}";
|
||||
|
||||
private string ConfirmButtonCss =>
|
||||
Dialog.Tone == UiDialogTone.Danger
|
||||
? "btn app-confirmation__button app-confirmation__button--danger"
|
||||
: "btn app-confirmation__button app-confirmation__button--primary";
|
||||
|
||||
private string ToneName => Dialog.Tone switch
|
||||
{
|
||||
UiDialogTone.Danger => "danger",
|
||||
UiDialogTone.Information => "information",
|
||||
_ => "warning"
|
||||
};
|
||||
|
||||
private string Eyebrow => Dialog.Tone switch
|
||||
{
|
||||
UiDialogTone.Danger => "Acción irreversible",
|
||||
UiDialogTone.Information => "Confirmación",
|
||||
_ => "Revisa antes de continuar"
|
||||
};
|
||||
|
||||
protected override void OnInitialized()
|
||||
{
|
||||
Dialog.Changed += HandleDialogChanged;
|
||||
}
|
||||
|
||||
protected override async Task OnAfterRenderAsync(bool firstRender)
|
||||
{
|
||||
if (!_focusPending || !Dialog.IsVisible)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_focusPending = false;
|
||||
await _cancelButton.FocusAsync();
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Dialog.Changed -= HandleDialogChanged;
|
||||
}
|
||||
|
||||
private void HandleDialogChanged()
|
||||
{
|
||||
_focusPending = Dialog.IsVisible;
|
||||
_ = InvokeAsync(StateHasChanged);
|
||||
}
|
||||
|
||||
private void HandleKeyDown(KeyboardEventArgs args)
|
||||
{
|
||||
if (string.Equals(args.Key, "Escape", StringComparison.Ordinal))
|
||||
{
|
||||
Dialog.Cancel();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,163 @@
|
||||
.app-confirmation-backdrop {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 5100;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
padding: 1.25rem;
|
||||
background: rgba(6, 22, 41, 0.64);
|
||||
backdrop-filter: blur(5px);
|
||||
overscroll-behavior: contain;
|
||||
}
|
||||
|
||||
.app-confirmation {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
width: min(560px, 100%);
|
||||
border: 1px solid rgba(255, 255, 255, 0.65);
|
||||
border-radius: 8px;
|
||||
background: #fff;
|
||||
box-shadow: 0 28px 80px rgba(5, 27, 54, 0.34);
|
||||
color: #12395f;
|
||||
}
|
||||
|
||||
.app-confirmation__accent {
|
||||
height: 0.35rem;
|
||||
background: #d49620;
|
||||
}
|
||||
|
||||
.app-confirmation--danger .app-confirmation__accent {
|
||||
background: #c93f4f;
|
||||
}
|
||||
|
||||
.app-confirmation--information .app-confirmation__accent {
|
||||
background: #2a5caa;
|
||||
}
|
||||
|
||||
.app-confirmation__content {
|
||||
display: grid;
|
||||
grid-template-columns: 3.25rem minmax(0, 1fr);
|
||||
gap: 1rem;
|
||||
padding: 1.5rem 1.5rem 1.1rem;
|
||||
}
|
||||
|
||||
.app-confirmation__icon {
|
||||
display: grid;
|
||||
place-items: center;
|
||||
width: 3.25rem;
|
||||
height: 3.25rem;
|
||||
border: 1px solid #efd8a8;
|
||||
border-radius: 50%;
|
||||
background: #fff6df;
|
||||
color: #8d5e08;
|
||||
font-size: 1.45rem;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.app-confirmation__icon--danger {
|
||||
border-color: #efc2c8;
|
||||
background: #fff0f2;
|
||||
color: #a82d3b;
|
||||
}
|
||||
|
||||
.app-confirmation__icon--information {
|
||||
border-color: #c4d6ef;
|
||||
background: #eef5ff;
|
||||
color: #214f91;
|
||||
}
|
||||
|
||||
.app-confirmation__copy {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.app-confirmation__eyebrow {
|
||||
margin-bottom: 0.3rem;
|
||||
color: #6a7786;
|
||||
font-size: 0.74rem;
|
||||
font-weight: 800;
|
||||
letter-spacing: 0;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.app-confirmation__title {
|
||||
margin: 0;
|
||||
color: #0a315c;
|
||||
font-size: 1.35rem;
|
||||
font-weight: 800;
|
||||
line-height: 1.25;
|
||||
}
|
||||
|
||||
.app-confirmation__message {
|
||||
margin: 0.65rem 0 0;
|
||||
color: #405f7d;
|
||||
line-height: 1.55;
|
||||
white-space: pre-line;
|
||||
}
|
||||
|
||||
.app-confirmation__actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 0.65rem;
|
||||
padding: 1rem 1.5rem 1.35rem;
|
||||
border-top: 1px solid #e4ebf2;
|
||||
background: #f8fafc;
|
||||
}
|
||||
|
||||
.app-confirmation__button {
|
||||
min-width: 8.5rem;
|
||||
min-height: 2.7rem;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.app-confirmation__button--primary {
|
||||
border-color: #24539a;
|
||||
background: #24539a;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.app-confirmation__button--primary:hover,
|
||||
.app-confirmation__button--primary:focus-visible {
|
||||
border-color: #193f79;
|
||||
background: #193f79;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.app-confirmation__button--danger {
|
||||
border-color: #b93343;
|
||||
background: #b93343;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.app-confirmation__button--danger:hover,
|
||||
.app-confirmation__button--danger:focus-visible {
|
||||
border-color: #922936;
|
||||
background: #922936;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
@media (max-width: 575.98px) {
|
||||
.app-confirmation-backdrop {
|
||||
align-items: end;
|
||||
padding: 0.75rem;
|
||||
}
|
||||
|
||||
.app-confirmation__content {
|
||||
grid-template-columns: 2.75rem minmax(0, 1fr);
|
||||
gap: 0.8rem;
|
||||
padding: 1.2rem 1rem 1rem;
|
||||
}
|
||||
|
||||
.app-confirmation__icon {
|
||||
width: 2.75rem;
|
||||
height: 2.75rem;
|
||||
}
|
||||
|
||||
.app-confirmation__actions {
|
||||
flex-direction: column-reverse;
|
||||
padding: 0.9rem 1rem 1rem;
|
||||
}
|
||||
|
||||
.app-confirmation__button {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,13 @@
|
||||
@inherits LayoutComponentBase
|
||||
@implements IDisposable
|
||||
@implements IAsyncDisposable
|
||||
@using System.Globalization
|
||||
@inject GestionaDenunciasAN.Models.UserState userState
|
||||
@inject IHttpContextAccessor HttpContextAccessor
|
||||
@inject IJSRuntime JSRuntime
|
||||
@inject NavigationManager Navigation
|
||||
@inject UiBusyService Busy
|
||||
@inject ApiDenunciasClient ApiDenuncias
|
||||
@inject ILogger<MainLayout> Logger
|
||||
|
||||
<div class="app-shell">
|
||||
<aside class="app-sidebar">
|
||||
@@ -20,11 +23,18 @@
|
||||
</div>
|
||||
|
||||
<div class="app-header__actions">
|
||||
<div class="app-status-pills">
|
||||
<div class="app-session-pill">
|
||||
<span class="app-session-pill__dot"></span>
|
||||
Sesion interna activa
|
||||
</div>
|
||||
|
||||
<div class="@EncryptionKeyPillCss" title="@EncryptionKeyTooltip">
|
||||
<span class="app-session-pill__dot"></span>
|
||||
@EncryptionKeyText
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button type="button" class="app-user-chip" @onclick="CerrarSesionAsync">
|
||||
<span class="bi bi-person-circle app-user-chip__icon" aria-hidden="true"></span>
|
||||
<span class="app-user-chip__text">
|
||||
@@ -42,6 +52,7 @@
|
||||
</div>
|
||||
|
||||
<BusyOverlay />
|
||||
<AppConfirmationDialog />
|
||||
|
||||
<div id="blazor-error-ui">
|
||||
An unhandled error has occurred.
|
||||
@@ -50,9 +61,20 @@
|
||||
</div>
|
||||
|
||||
@code {
|
||||
private static readonly TimeSpan GlobalLeaksHeartbeatInterval = TimeSpan.FromSeconds(30);
|
||||
private static readonly TimeSpan GlobalLeaksIdleTimeout = TimeSpan.FromMinutes(30);
|
||||
|
||||
private string CurrentPageTitle { get; set; } = "Portal de gestion";
|
||||
private string CurrentPageDescription { get; set; } =
|
||||
"Entrada, revision y tramitacion coordinada de denuncias y actualizaciones.";
|
||||
private string EncryptionKeyText { get; set; } = "Clave diaria: cargando...";
|
||||
private string EncryptionKeyTooltip { get; set; } = "Consultando la ultima clave diaria de cifrado activa en la API.";
|
||||
private string EncryptionKeyPillCss { get; set; } = "app-session-pill app-key-pill";
|
||||
private readonly CancellationTokenSource _heartbeatCancellation = new();
|
||||
private PeriodicTimer? _heartbeatTimer;
|
||||
private Task? _heartbeatTask;
|
||||
private bool _globalLeaksSessionClearedForIdle;
|
||||
private DateTimeOffset _lastHeartbeatWarningAtUtc = DateTimeOffset.MinValue;
|
||||
|
||||
private string DisplayUsername =>
|
||||
string.IsNullOrWhiteSpace(userState?.NombreUsu)
|
||||
@@ -65,20 +87,205 @@
|
||||
RefreshLayoutState();
|
||||
}
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
await LoadEncryptionKeyStatusAsync();
|
||||
}
|
||||
|
||||
protected override void OnParametersSet()
|
||||
{
|
||||
RefreshLayoutState();
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
protected override async Task OnAfterRenderAsync(bool firstRender)
|
||||
{
|
||||
if (!firstRender)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
await JSRuntime.InvokeVoidAsync("appGlobalLeaksActivity.start");
|
||||
}
|
||||
catch (JSException ex)
|
||||
{
|
||||
Logger.LogError(
|
||||
ex,
|
||||
"No se ha podido iniciar el control de actividad para la sesion GlobalLeaks.");
|
||||
return;
|
||||
}
|
||||
|
||||
_heartbeatTimer = new PeriodicTimer(GlobalLeaksHeartbeatInterval);
|
||||
_heartbeatTask = RunGlobalLeaksHeartbeatAsync(_heartbeatCancellation.Token);
|
||||
}
|
||||
|
||||
public async ValueTask DisposeAsync()
|
||||
{
|
||||
Navigation.LocationChanged -= HandleLocationChanged;
|
||||
_heartbeatCancellation.Cancel();
|
||||
_heartbeatTimer?.Dispose();
|
||||
|
||||
if (_heartbeatTask is not null)
|
||||
{
|
||||
try
|
||||
{
|
||||
await _heartbeatTask;
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
await JSRuntime.InvokeVoidAsync("appGlobalLeaksActivity.stop");
|
||||
}
|
||||
catch (JSDisconnectedException)
|
||||
{
|
||||
}
|
||||
catch (ObjectDisposedException)
|
||||
{
|
||||
}
|
||||
|
||||
_heartbeatCancellation.Dispose();
|
||||
}
|
||||
|
||||
private void HandleLocationChanged(object? sender, LocationChangedEventArgs args)
|
||||
{
|
||||
RefreshLayoutState();
|
||||
_ = InvokeAsync(StateHasChanged);
|
||||
_ = InvokeAsync(async () =>
|
||||
{
|
||||
await LoadEncryptionKeyStatusAsync();
|
||||
StateHasChanged();
|
||||
});
|
||||
}
|
||||
|
||||
private async Task LoadEncryptionKeyStatusAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
var config = await ApiDenuncias.GetAppConfigurationAsync();
|
||||
var keyDate = ToSpanishDateText(config.LatestEncryptionKeyDate);
|
||||
var keyStatus = ToSpanishKeyStatus(config.LatestEncryptionKeyStatus);
|
||||
|
||||
if (string.IsNullOrWhiteSpace(keyDate))
|
||||
{
|
||||
EncryptionKeyText = "Clave diaria: no disponible";
|
||||
EncryptionKeyTooltip = "No se ha encontrado informacion de la clave diaria de cifrado.";
|
||||
EncryptionKeyPillCss = "app-session-pill app-key-pill app-key-pill--warning";
|
||||
return;
|
||||
}
|
||||
|
||||
EncryptionKeyText = string.IsNullOrWhiteSpace(keyStatus)
|
||||
? $"Clave diaria: {keyDate}"
|
||||
: $"Clave diaria: {keyDate} ({keyStatus})";
|
||||
EncryptionKeyTooltip = "Ultima clave diaria de cifrado registrada por la API.";
|
||||
EncryptionKeyPillCss = string.Equals(config.LatestEncryptionKeyStatus, "active", StringComparison.OrdinalIgnoreCase)
|
||||
? "app-session-pill app-key-pill"
|
||||
: "app-session-pill app-key-pill app-key-pill--warning";
|
||||
}
|
||||
catch
|
||||
{
|
||||
EncryptionKeyText = "Clave diaria: no disponible";
|
||||
EncryptionKeyTooltip = "No se ha podido consultar la clave diaria de cifrado.";
|
||||
EncryptionKeyPillCss = "app-session-pill app-key-pill app-key-pill--warning";
|
||||
}
|
||||
}
|
||||
|
||||
private async Task RunGlobalLeaksHeartbeatAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
try
|
||||
{
|
||||
while (_heartbeatTimer is not null &&
|
||||
await _heartbeatTimer.WaitForNextTickAsync(cancellationToken))
|
||||
{
|
||||
await InvokeAsync(() => MaintainGlobalLeaksSessionAsync(cancellationToken));
|
||||
}
|
||||
}
|
||||
catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
}
|
||||
catch (ObjectDisposedException)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
private async Task MaintainGlobalLeaksSessionAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
try
|
||||
{
|
||||
var idleMilliseconds = await JSRuntime.InvokeAsync<double>(
|
||||
"appGlobalLeaksActivity.getIdleMilliseconds",
|
||||
cancellationToken);
|
||||
|
||||
if (idleMilliseconds >= GlobalLeaksIdleTimeout.TotalMilliseconds)
|
||||
{
|
||||
if (!_globalLeaksSessionClearedForIdle)
|
||||
{
|
||||
await ApiDenuncias.ClearGlobalLeaksSessionAsync(cancellationToken);
|
||||
_globalLeaksSessionClearedForIdle = true;
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
_globalLeaksSessionClearedForIdle = false;
|
||||
await ApiDenuncias.KeepGlobalLeaksSessionAliveAsync(cancellationToken);
|
||||
}
|
||||
catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
}
|
||||
catch (JSDisconnectedException)
|
||||
{
|
||||
}
|
||||
catch (UnauthorizedAccessException)
|
||||
{
|
||||
_heartbeatCancellation.Cancel();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
var now = DateTimeOffset.UtcNow;
|
||||
if (now - _lastHeartbeatWarningAtUtc >= TimeSpan.FromMinutes(5))
|
||||
{
|
||||
_lastHeartbeatWarningAtUtc = now;
|
||||
Logger.LogWarning(
|
||||
ex,
|
||||
"No se ha podido ejecutar el mantenimiento de la sesion GlobalLeaks.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static string ToSpanishDateText(string? value)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(value))
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
return DateOnly.TryParseExact(
|
||||
value.Trim(),
|
||||
"yyyy-MM-dd",
|
||||
CultureInfo.InvariantCulture,
|
||||
DateTimeStyles.None,
|
||||
out var date)
|
||||
? date.ToString("dd/MM/yyyy", CultureInfo.InvariantCulture)
|
||||
: value.Trim();
|
||||
}
|
||||
|
||||
private static string ToSpanishKeyStatus(string? status)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(status))
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
return status.Trim().ToLowerInvariant() switch
|
||||
{
|
||||
"active" => "activa",
|
||||
"purged" => "purgada",
|
||||
_ => status.Trim()
|
||||
};
|
||||
}
|
||||
|
||||
private void RefreshLayoutState()
|
||||
@@ -98,7 +305,7 @@
|
||||
|
||||
return path.ToLowerInvariant() switch
|
||||
{
|
||||
"" or "gestionzip" => (
|
||||
"" or "entrada" => (
|
||||
"Entrada de denuncias",
|
||||
"Importa lo nuevo desde GlobalLeaks, revisa el seguimiento por usuario y decide si habra expediente nuevo o actualizacion."),
|
||||
"pendientes" => (
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
@using System.Security.Claims
|
||||
|
||||
<div class="nav-shell">
|
||||
<div class="nav-brand">
|
||||
<img class="nav-brand__logo"
|
||||
@@ -14,7 +16,7 @@
|
||||
<div class="nav-section">
|
||||
<span class="nav-section__label">Operativa diaria</span>
|
||||
|
||||
<NavLink class="menu-link" href="/GestionZip" Match="NavLinkMatch.All">
|
||||
<NavLink class="menu-link" href="/Entrada" Match="NavLinkMatch.All">
|
||||
<span class="menu-link__icon bi bi-box-seam" aria-hidden="true"></span>
|
||||
<span class="menu-link__content">
|
||||
<span class="menu-link__title">Entrada</span>
|
||||
@@ -66,6 +68,10 @@
|
||||
</span>
|
||||
</NavLink>
|
||||
|
||||
<AuthorizeView>
|
||||
<Authorized Context="authContext">
|
||||
@if (IsConfigurationUser(authContext.User))
|
||||
{
|
||||
<NavLink class="menu-link" href="/Configuracion" Match="NavLinkMatch.All">
|
||||
<span class="menu-link__icon bi bi-gear" aria-hidden="true"></span>
|
||||
<span class="menu-link__content">
|
||||
@@ -73,6 +79,9 @@
|
||||
<span class="menu-link__meta">Fecha de corte y purga manual</span>
|
||||
</span>
|
||||
</NavLink>
|
||||
}
|
||||
</Authorized>
|
||||
</AuthorizeView>
|
||||
</div>
|
||||
|
||||
<div class="nav-section nav-section--footer">
|
||||
@@ -88,3 +97,19 @@
|
||||
</div>
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
@code {
|
||||
private static readonly HashSet<string> ConfigurationUsers = new(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
"pcornejo",
|
||||
"Rgarciaglbk",
|
||||
"eaguilarGestor"
|
||||
};
|
||||
|
||||
private static bool IsConfigurationUser(ClaimsPrincipal user)
|
||||
{
|
||||
var username = user.Identity?.Name?.Trim();
|
||||
return !string.IsNullOrWhiteSpace(username) &&
|
||||
ConfigurationUsers.Contains(username);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
@page "/Actualizaciones"
|
||||
@page "/Actualizaciones"
|
||||
@rendermode InteractiveServer
|
||||
@attribute [Authorize]
|
||||
@using GestionaDenuncias.Shared.Models
|
||||
@@ -16,6 +16,7 @@
|
||||
@inject IDenunciaStore DenunciaStore
|
||||
@inject ApiDenunciasClient ApiDenuncias
|
||||
@inject UiBusyService Busy
|
||||
@inject UiDialogService Dialogs
|
||||
|
||||
<PageTitle>Actualizaciones</PageTitle>
|
||||
|
||||
@@ -58,7 +59,7 @@
|
||||
margin: 1rem 0 0.5rem;
|
||||
}
|
||||
|
||||
/* Tarjetas de actualizaci<EFBFBD>n (azules) */
|
||||
/* Tarjetas de actualización (azules) */
|
||||
.collapse-card.update-card {
|
||||
background-color: #e3f2fd;
|
||||
}
|
||||
@@ -73,7 +74,7 @@
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
/* === Est<EFBFBD>tica de modal igual que en Pendientes === */
|
||||
/* === Estética de modal igual que en Pendientes === */
|
||||
|
||||
.custom-modal {
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
@@ -162,7 +163,21 @@ else
|
||||
data-bs-target="#@collapseId"
|
||||
aria-expanded="false"
|
||||
aria-controls="@collapseId">
|
||||
<h5 class="mb-0">Denuncia ID: @denuncia.Id_Denuncia (Actualizaci<63>n)</h5>
|
||||
<div class="d-flex flex-wrap align-items-center gap-2">
|
||||
<h5 class="mb-0">Denuncia ID: @denuncia.Id_Denuncia (Actualización)</h5>
|
||||
@if (!string.IsNullOrWhiteSpace(denuncia.OwnerUsername))
|
||||
{
|
||||
<span class="badge @(denuncia.OwnedByCurrentUser ? "text-bg-secondary" : "text-bg-warning")">
|
||||
Responsable: @denuncia.OwnerUsername
|
||||
</span>
|
||||
}
|
||||
@if (denuncia.OwnerWorkGroups.Count > 0)
|
||||
{
|
||||
<span class="badge text-bg-light">
|
||||
Grupo: @string.Join(" / ", denuncia.OwnerWorkGroups)
|
||||
</span>
|
||||
}
|
||||
</div>
|
||||
|
||||
<div class="d-flex align-items-center">
|
||||
<div class="text-muted small me-3">
|
||||
@@ -176,19 +191,11 @@ else
|
||||
}
|
||||
</div>
|
||||
|
||||
<button type="button"
|
||||
class="btn btn-outline-secondary btn-sm me-2"
|
||||
title="Marcar como pendiente (no actualizaci<63>n)"
|
||||
@onclick:stopPropagation="true"
|
||||
@onclick="() => MoverAPendientes(denuncia)">
|
||||
Mover a Pendientes
|
||||
</button>
|
||||
|
||||
<button type="button"
|
||||
class="btn btn-success btn-sm"
|
||||
@onclick:stopPropagation="true"
|
||||
@onclick="() => OpenEnviarAGestionaModal(denuncia)">
|
||||
Configurar subida
|
||||
Configurar actualizacion expediente
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -204,7 +211,7 @@ else
|
||||
}
|
||||
@if (!string.IsNullOrWhiteSpace(denuncia.ExpedienteGestionaMostrable))
|
||||
{
|
||||
<dt class="col-sm-3">N<EFBFBD> expediente Gestiona</dt>
|
||||
<dt class="col-sm-3">Nº expediente Gestiona</dt>
|
||||
<dd class="col-sm-9">@denuncia.ExpedienteGestionaMostrable</dd>
|
||||
}
|
||||
@if (!string.IsNullOrWhiteSpace(denuncia.Etiqueta))
|
||||
@@ -245,7 +252,7 @@ else
|
||||
}
|
||||
@if (!string.IsNullOrWhiteSpace(denuncia.RazonSocialResuelta))
|
||||
{
|
||||
<dt class="col-sm-3">Raz<EFBFBD>n social</dt>
|
||||
<dt class="col-sm-3">Razón social</dt>
|
||||
<dd class="col-sm-9">@denuncia.RazonSocialResuelta</dd>
|
||||
}
|
||||
@if (!string.IsNullOrWhiteSpace(denuncia.NombreResuelto))
|
||||
@@ -255,12 +262,12 @@ else
|
||||
}
|
||||
@if (!string.IsNullOrWhiteSpace(denuncia.PrimerApellidoResuelto))
|
||||
{
|
||||
<dt class="col-sm-3">1<EFBFBD> apellido</dt>
|
||||
<dt class="col-sm-3">1º apellido</dt>
|
||||
<dd class="col-sm-9">@denuncia.PrimerApellidoResuelto</dd>
|
||||
}
|
||||
@if (!string.IsNullOrWhiteSpace(denuncia.SegundoApellidoResuelto))
|
||||
{
|
||||
<dt class="col-sm-3">2<EFBFBD> apellido</dt>
|
||||
<dt class="col-sm-3">2º apellido</dt>
|
||||
<dd class="col-sm-9">@denuncia.SegundoApellidoResuelto</dd>
|
||||
}
|
||||
@if (!string.IsNullOrWhiteSpace(denuncia.ApellidosResueltos))
|
||||
@@ -280,14 +287,14 @@ else
|
||||
<dl class="row">
|
||||
<dt class="col-sm-3">Asunto</dt>
|
||||
<dd class="col-sm-9">@denuncia.Asunto</dd>
|
||||
<dt class="col-sm-3">A Qui<EFBFBD>n</dt>
|
||||
<dt class="col-sm-3">A Quién</dt>
|
||||
<dd class="col-sm-9">@denuncia.A_Quien_Denuncia</dd>
|
||||
@if (!string.IsNullOrWhiteSpace(denuncia.DenunciadoDetalle))
|
||||
{
|
||||
<dt class="col-sm-3">Detalle denunciado</dt>
|
||||
<dd class="col-sm-9">@denuncia.DenunciadoDetalle</dd>
|
||||
}
|
||||
<dt class="col-sm-3">Descripci<EFBFBD>n</dt>
|
||||
<dt class="col-sm-3">Descripción</dt>
|
||||
<dd class="col-sm-9">@denuncia.Descripcion_Denuncia</dd>
|
||||
@if (!string.IsNullOrWhiteSpace(denuncia.OrganismoDenunciado))
|
||||
{
|
||||
@@ -296,7 +303,7 @@ else
|
||||
}
|
||||
@if (!string.IsNullOrWhiteSpace(denuncia.SolicitaProteccion))
|
||||
{
|
||||
<dt class="col-sm-3">Solicita protecci<EFBFBD>n</dt>
|
||||
<dt class="col-sm-3">Solicita protección</dt>
|
||||
<dd class="col-sm-9">@denuncia.SolicitaProteccion</dd>
|
||||
}
|
||||
@if (!string.IsNullOrWhiteSpace(denuncia.MedidasProteccionSolicitadas))
|
||||
@@ -313,7 +320,7 @@ else
|
||||
}
|
||||
@if (!string.IsNullOrWhiteSpace(denuncia.AutorizaRemision))
|
||||
{
|
||||
<dt class="col-sm-3">Autoriza remisi<EFBFBD>n</dt>
|
||||
<dt class="col-sm-3">Autoriza remisión</dt>
|
||||
<dd class="col-sm-9">@denuncia.AutorizaRemision</dd>
|
||||
}
|
||||
</dl>
|
||||
@@ -324,14 +331,14 @@ else
|
||||
@if (camposFormulario.Count > 0)
|
||||
{
|
||||
<h5 class="section-heading">Formulario Original</h5>
|
||||
@foreach (var grupoCampos in camposFormulario.GroupBy(field => string.IsNullOrWhiteSpace(field.Section) ? "Sin secci<EFBFBD>n" : field.Section))
|
||||
@foreach (var grupoCampos in camposFormulario.GroupBy(field => string.IsNullOrWhiteSpace(field.Section) ? "Sin sección" : field.Section))
|
||||
{
|
||||
<h6 class="mt-3">@grupoCampos.Key</h6>
|
||||
<dl class="row">
|
||||
@foreach (var campo in grupoCampos)
|
||||
{
|
||||
<dt class="col-sm-4">@campo.Label</dt>
|
||||
<dd class="col-sm-8">@(string.IsNullOrWhiteSpace(campo.Value) ? "<EFBFBD>" : campo.Value)</dd>
|
||||
<dd class="col-sm-8">@(string.IsNullOrWhiteSpace(campo.Value) ? "—" : campo.Value)</dd>
|
||||
}
|
||||
</dl>
|
||||
}
|
||||
@@ -347,7 +354,7 @@ else
|
||||
<th>Nombre</th>
|
||||
<th>Fecha</th>
|
||||
<th>Motivo</th>
|
||||
<th>Tama<EFBFBD>o (bytes)</th>
|
||||
<th>Tamaño (bytes)</th>
|
||||
<th>Ver</th>
|
||||
</tr>
|
||||
</thead>
|
||||
@@ -396,7 +403,7 @@ else
|
||||
}
|
||||
else
|
||||
{
|
||||
<span class="text-muted"><EFBFBD></span>
|
||||
<span class="text-muted">—</span>
|
||||
}
|
||||
</td>
|
||||
</tr>
|
||||
@@ -447,19 +454,19 @@ else
|
||||
{
|
||||
<div class="alert alert-info d-flex align-items-center" role="alert">
|
||||
<div class="spinner-border spinner-border-sm me-2" role="status" aria-hidden="true"></div>
|
||||
Buscando expediente existente en Gestiona<EFBFBD>
|
||||
Buscando expediente existente en Gestiona…
|
||||
</div>
|
||||
}
|
||||
else if (autoSearchTried && !string.IsNullOrWhiteSpace(autoFoundFileUrl))
|
||||
{
|
||||
<div class="alert alert-success" role="alert">
|
||||
<div class="fw-semibold">Expediente detectado en Gestiona.</div>
|
||||
<div>Asunto: <strong>@(autoFoundTitle ?? "(sin t<EFBFBD>tulo)")</strong></div>
|
||||
<div>Asunto: <strong>@(autoFoundTitle ?? "(sin título)")</strong></div>
|
||||
<div class="text-muted small">@autoFoundFileUrl</div>
|
||||
<div class="form-check mt-2">
|
||||
<input class="form-check-input" type="checkbox" id="chkUsarDetectado" @bind="useAutoFoundExpediente" />
|
||||
<label class="form-check-label" for="chkUsarDetectado">
|
||||
A<EFBFBD>adir documentos a este expediente.
|
||||
Añadir documentos a este expediente.
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
@@ -468,12 +475,7 @@ else
|
||||
{
|
||||
<div class="alert alert-warning" role="alert">
|
||||
<div class="fw-semibold">No se ha detectado expediente en Gestiona por asunto.</div>
|
||||
<div class="small">Puede que esto no sea una actualizaci<EFBFBD>n del mismo caso.</div>
|
||||
<div class="mt-2">
|
||||
<button class="btn btn-outline-secondary btn-sm" @onclick="MoverADenunciasPendientes">
|
||||
Mover a Pendientes
|
||||
</button>
|
||||
</div>
|
||||
<div class="small">Puede que esto no sea una actualización del mismo caso.</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
@@ -491,17 +493,25 @@ else
|
||||
</div>
|
||||
}
|
||||
|
||||
<h6 class="modal-section-heading">Descripci<63>n</h6>
|
||||
@if (!string.IsNullOrWhiteSpace(selectedDenuncias.ExpedienteGestionaMostrable))
|
||||
{
|
||||
<div class="alert alert-info" role="alert">
|
||||
<div class="fw-semibold">Expediente de Gestiona donde se hará la actualización</div>
|
||||
<div>Nº expediente: <strong>@selectedDenuncias.ExpedienteGestionaMostrable</strong></div>
|
||||
</div>
|
||||
}
|
||||
|
||||
<h6 class="modal-section-heading">Descripción</h6>
|
||||
<div class="mb-3">
|
||||
<input type="text" class="form-control" @bind="nuevoAsunto" placeholder="Ingrese el nombre de la denuncia" />
|
||||
<input type="text" class="form-control" @bind="nuevoAsunto" readonly />
|
||||
</div>
|
||||
|
||||
<h6 class="modal-section-heading">Nombre de los documentos</h6>
|
||||
<div class="mb-3">
|
||||
<input type="text" class="form-control" @bind="nombreDocumentos"
|
||||
placeholder="Ej.: Gesti<EFBFBD>n AN (Documento Adjunto 1 Gesti<EFBFBD>n AN...)" />
|
||||
placeholder="Ej.: Gestión AN (Documento Adjunto 1 Gestión AN...)" />
|
||||
<small class="text-muted">
|
||||
Se aplica al modo individual. <em>report.txt</em> se sube como <strong>Denuncia</strong> si entra en esta actualizaci<EFBFBD>n.
|
||||
Se aplica al modo individual. <em>report.txt</em> se sube como <strong>Denuncia</strong> si entra en esta actualización.
|
||||
</small>
|
||||
</div>
|
||||
|
||||
@@ -509,7 +519,7 @@ else
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="radio" name="uploadMode" id="modoMerge"
|
||||
checked='@(uploadMode == "merge")' @onclick='() => uploadMode = "merge"' />
|
||||
<label class="form-check-label" for="modoMerge">Unir todos los ficheros en un <EFBFBD>nico PDF</label>
|
||||
<label class="form-check-label" for="modoMerge">Unir todos los ficheros en un único PDF</label>
|
||||
</div>
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="radio" name="uploadMode" id="modoIndividual"
|
||||
@@ -522,18 +532,13 @@ else
|
||||
<input class="form-check-input" type="radio" name="selectedGroup" id="grupo600"
|
||||
checked='@(selectedGroup == "600")' @onclick='() => selectedGroup = "600"' />
|
||||
<label class="form-check-label" for="grupo600">
|
||||
600. Asuntos Jur<EFBFBD>dicos y Protecci<EFBFBD>n a la Persona Denunciante
|
||||
600. Asuntos Jurídicos y Protección a la Persona Denunciante
|
||||
</label>
|
||||
</div>
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="radio" name="selectedGroup" id="grupo510"
|
||||
checked='@(selectedGroup == "510")' @onclick='() => selectedGroup = "510"' />
|
||||
<label class="form-check-label" for="grupo510">510. SDI <EFBFBD> Investigaci<EFBFBD>n Entradas</label>
|
||||
</div>
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="radio" name="selectedGroup" id="grupo700"
|
||||
checked='@(selectedGroup == "700")' @onclick='() => selectedGroup = "700"' />
|
||||
<label class="form-check-label" for="grupo700">700. RESPONSABLE DEL SERVICIO</label>
|
||||
<label class="form-check-label" for="grupo510">510. SDI – Investigación Entradas</label>
|
||||
</div>
|
||||
|
||||
@{
|
||||
@@ -542,13 +547,13 @@ else
|
||||
<h6 class="modal-section-heading mt-3">Tercero (denunciante)</h6>
|
||||
|
||||
<div class="alert alert-light border mb-3">
|
||||
Los datos del tercero se cargan autom<EFBFBD>ticamente desde la denuncia y no se pueden editar aqu<EFBFBD>.
|
||||
Los datos del tercero se cargan automáticamente desde la denuncia y no se pueden editar aquí.
|
||||
</div>
|
||||
|
||||
@if (modalThirdParty.IsAnonymous)
|
||||
{
|
||||
<div class="alert alert-warning mb-3">
|
||||
Denuncia an<EFBFBD>nima. Se enlazar<EFBFBD> autom<EFBFBD>ticamente el tercero <strong>00000000T</strong>.
|
||||
Denuncia anónima. Se enlazará automáticamente el tercero <strong>00000000T</strong>.
|
||||
</div>
|
||||
}
|
||||
|
||||
@@ -577,7 +582,7 @@ else
|
||||
{
|
||||
<div class="row g-2">
|
||||
<div class="col-12 mb-2">
|
||||
<label class="form-label">Raz<EFBFBD>n social</label>
|
||||
<label class="form-label">Razón social</label>
|
||||
<input class="form-control" value="@GetReadOnlyValue(selectedDenuncias.RazonSocialResuelta)" readonly />
|
||||
</div>
|
||||
</div>
|
||||
@@ -590,11 +595,11 @@ else
|
||||
<input class="form-control" value="@GetReadOnlyValue(selectedDenuncias.NombreResuelto)" readonly />
|
||||
</div>
|
||||
<div class="col-4 mb-2">
|
||||
<label class="form-label">1<EFBFBD> apellido</label>
|
||||
<label class="form-label">1º apellido</label>
|
||||
<input class="form-control" value="@GetReadOnlyValue(selectedDenuncias.PrimerApellidoResuelto)" readonly />
|
||||
</div>
|
||||
<div class="col-4 mb-2">
|
||||
<label class="form-label">2<EFBFBD> apellido</label>
|
||||
<label class="form-label">2º apellido</label>
|
||||
<input class="form-control" value="@GetReadOnlyValue(selectedDenuncias.SegundoApellidoResuelto)" readonly />
|
||||
</div>
|
||||
</div>
|
||||
@@ -604,14 +609,14 @@ else
|
||||
{
|
||||
<div class="row g-2">
|
||||
<div class="col-12 mb-2">
|
||||
<label class="form-label">Direcci<EFBFBD>n postal</label>
|
||||
<label class="form-label">Dirección postal</label>
|
||||
<textarea class="form-control" rows="2" readonly>@BuildPostalAddressSummary(selectedDenuncias)</textarea>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
<small class="text-muted">
|
||||
Antes de subir la actualizaci<EFBFBD>n se comprobar<EFBFBD> el tercero extra<EFBFBD>do del formulario y, si no est<EFBFBD> enlazado al expediente, se enlazar<EFBFBD>.
|
||||
Antes de subir la actualización se comprobará el tercero extraído del formulario y, si no está enlazado al expediente, se enlazará.
|
||||
</small>
|
||||
</div>
|
||||
|
||||
@@ -639,14 +644,14 @@ else
|
||||
// --- Modal / estado ---
|
||||
private bool showModal = false;
|
||||
private bool isUploading = false;
|
||||
private string uploadMode = "merge";
|
||||
private string uploadMode = "individual";
|
||||
private string selectedGroup = "600";
|
||||
private string nuevoAsunto = "";
|
||||
private string nombreDocumentos = "";
|
||||
private DenunciasGestiona? selectedDenuncias;
|
||||
private ThirdPartyIdentityData? selectedThirdParty;
|
||||
|
||||
// --- Detecci<EFBFBD>n autom<EFBFBD>tica en Gestiona por asunto ---
|
||||
// --- Detección automática en Gestiona por asunto ---
|
||||
private bool autoSearchLoading = false;
|
||||
private bool autoSearchTried = false;
|
||||
private string? autoFoundFileUrl = null;
|
||||
@@ -668,19 +673,15 @@ else
|
||||
var config = await ApiDenuncias.GetAppConfigurationAsync();
|
||||
externalUpdateCutoffDate = ParseConfiguredCutoffDate(config.ExternalUpdateCutoffDate);
|
||||
var todas = await CargarDenunciasJsonAsync();
|
||||
|
||||
foreach (var d in todas.Where(x => x.ProcedureId == Guid.Empty))
|
||||
{
|
||||
d.ProcedureId = Guid.Parse("82722c9b-cecc-4299-8a7b-ce5abeb8170b");
|
||||
d.GroupId = Guid.Parse("6dbfc433-1eb6-4b9a-a533-bfebc652c101");
|
||||
}
|
||||
|
||||
actualizaciones = todas
|
||||
.Where(d => d.EsActualizacion)
|
||||
.OrderByDescending(d => d.FechaSubidaAGestiona != DateTime.MinValue ? d.FechaSubidaAGestiona : d.Fecha)
|
||||
.ToList();
|
||||
|
||||
ficherosAdjuntos = await CargarFicherosPorDenunciaAsync(actualizaciones);
|
||||
actualizaciones = actualizaciones
|
||||
.Where(d => ficherosAdjuntos.ContainsKey(d.Id_Denuncia))
|
||||
.ToList();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
@@ -715,12 +716,26 @@ else
|
||||
return result;
|
||||
}
|
||||
|
||||
private void OpenEnviarAGestionaModal(DenunciasGestiona d)
|
||||
private async Task OpenEnviarAGestionaModal(DenunciasGestiona d)
|
||||
{
|
||||
if (d.RequiresOwnerConfirmation)
|
||||
{
|
||||
var confirmed = await Dialogs.ConfirmAsync(
|
||||
"Denuncia asignada a otro usuario",
|
||||
$"La denuncia #{d.Id_Denuncia} está asignada a {d.OwnerUsername}. " +
|
||||
"Puedes actualizarla porque pertenece a un usuario de tu grupo. La acción quedará registrada con tu usuario.",
|
||||
"Configurar actualización");
|
||||
if (!confirmed)
|
||||
{
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
selectedDenuncias = d;
|
||||
nuevoAsunto = string.IsNullOrWhiteSpace(d.NombreDenuncia) ? $"Denuncia-{d.Id_Denuncia}-CD" : d.NombreDenuncia;
|
||||
nombreDocumentos = "";
|
||||
selectedThirdParty = ThirdPartyIdentityData.FromComplaint(d);
|
||||
selectedGroup = NormalizeUpdateGroup(d.UltimoGrupoAsignadoGestiona);
|
||||
operationError = string.Empty;
|
||||
operationNotice = string.Empty;
|
||||
|
||||
@@ -729,6 +744,12 @@ else
|
||||
useAutoFoundExpediente = true;
|
||||
autoSearchTried = false;
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(d.Expediente_Gestiona) &&
|
||||
string.IsNullOrWhiteSpace(d.CodigoExpedienteGestiona))
|
||||
{
|
||||
await SincronizarExpedienteGestionaAsync(d, d.Expediente_Gestiona);
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(d.Expediente_Gestiona))
|
||||
{
|
||||
autoSearchLoading = true;
|
||||
@@ -737,7 +758,7 @@ else
|
||||
|
||||
try
|
||||
{
|
||||
// B<EFBFBD>squeda autom<EFBFBD>tica desactivada temporalmente
|
||||
// Búsqueda automática desactivada temporalmente
|
||||
}
|
||||
catch
|
||||
{
|
||||
@@ -799,48 +820,20 @@ else
|
||||
return new string(clean.Where(ch => ch <= 127).ToArray());
|
||||
}
|
||||
|
||||
private string BuildMergedAttachmentsFileName(int denunciaId, DateTime timestampUtc)
|
||||
{
|
||||
var baseName = string.IsNullOrWhiteSpace(nombreDocumentos)
|
||||
? $"Adjuntos {denunciaId}_{timestampUtc:yyyyMMddHHmmss}"
|
||||
: nombreDocumentos.Trim();
|
||||
|
||||
return FixFileName($"{Path.GetFileNameWithoutExtension(baseName)}.pdf");
|
||||
}
|
||||
|
||||
private async Task ActualizarDenunciaAsync(DenunciasGestiona d)
|
||||
{
|
||||
await DenunciaStore.UpsertDenunciaAsync(d);
|
||||
}
|
||||
|
||||
private async Task MoverADenunciasPendientes()
|
||||
{
|
||||
if (selectedDenuncias is null) return;
|
||||
|
||||
selectedDenuncias.EsActualizacion = false;
|
||||
selectedDenuncias.EnGestiona = false;
|
||||
selectedDenuncias.EnRechazada = false;
|
||||
selectedDenuncias.Expediente_Gestiona = "Pendiente";
|
||||
selectedDenuncias.FechaSubidaAGestiona = DateTime.MinValue;
|
||||
selectedDenuncias.ArchivoElegido = string.Empty;
|
||||
|
||||
await ActualizarDenunciaAsync(selectedDenuncias);
|
||||
|
||||
actualizaciones.RemoveAll(x => x.Id_Denuncia == selectedDenuncias.Id_Denuncia);
|
||||
|
||||
CloseModal();
|
||||
StateHasChanged();
|
||||
}
|
||||
|
||||
private async Task MoverAPendientes(DenunciasGestiona d)
|
||||
{
|
||||
if (d is null) return;
|
||||
|
||||
d.EsActualizacion = false;
|
||||
d.EnGestiona = false;
|
||||
d.EnRechazada = false;
|
||||
d.Expediente_Gestiona = "Pendiente";
|
||||
d.FechaSubidaAGestiona = DateTime.MinValue;
|
||||
d.ArchivoElegido = string.Empty;
|
||||
|
||||
await ActualizarDenunciaAsync(d);
|
||||
|
||||
actualizaciones.RemoveAll(x => x.Id_Denuncia == d.Id_Denuncia);
|
||||
|
||||
StateHasChanged();
|
||||
}
|
||||
|
||||
private async Task ConfirmarEnvio()
|
||||
{
|
||||
if (selectedDenuncias == null) return;
|
||||
@@ -852,11 +845,19 @@ else
|
||||
return;
|
||||
}
|
||||
|
||||
if (!await ConfirmSelectedGroupAsync())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
isUploading = true;
|
||||
operationError = string.Empty;
|
||||
operationNotice = string.Empty;
|
||||
selectedGroup = NormalizeUpdateGroup(selectedGroup);
|
||||
var pendingUpdateSource = selectedDenuncias.PendingUpdateSource;
|
||||
var uploadType = GetUpdateUploadType(pendingUpdateSource, selectedGroup);
|
||||
using var busy = Busy.Show(
|
||||
"Enviando actualizacion",
|
||||
"Preparando expediente, carpeta de actualizacion y documentos.");
|
||||
@@ -889,12 +890,13 @@ else
|
||||
if (!todos.Any())
|
||||
{
|
||||
operationError = ficherosVacios.Count == 0
|
||||
? $"La denuncia #{selectedDenuncias.Id_Denuncia} no tiene ficheros pendientes para subir como actualizaci<EFBFBD>n."
|
||||
: $"La denuncia #{selectedDenuncias.Id_Denuncia} solo tiene ficheros vac<EFBFBD>os y no se ha subido nada. Ficheros omitidos: {string.Join(", ", ficherosVacios)}.";
|
||||
? $"La denuncia #{selectedDenuncias.Id_Denuncia} no tiene ficheros pendientes para subir como actualización."
|
||||
: $"La denuncia #{selectedDenuncias.Id_Denuncia} solo tiene ficheros vacíos y no se ha subido nada. Ficheros omitidos: {string.Join(", ", ficherosVacios)}.";
|
||||
return;
|
||||
}
|
||||
|
||||
// 2) Determinar expediente destino
|
||||
var expedienteCreadoEnGestiona = false;
|
||||
string fileUrl;
|
||||
if (selectedDenuncias.EnGestiona && !string.IsNullOrWhiteSpace(selectedDenuncias.Expediente_Gestiona))
|
||||
{
|
||||
@@ -910,7 +912,6 @@ else
|
||||
{
|
||||
Busy.Update(message: "Creando expediente nuevo en Gestiona.", detail: "Paso 2 de 8");
|
||||
var createdFile = await ApiDenuncias.CreateGestionaFileAsync(
|
||||
selectedDenuncias.ProcedureId,
|
||||
nuevoAsunto,
|
||||
"RQ2ZLC - Expediente de Denuncias",
|
||||
"3109963"
|
||||
@@ -920,33 +921,29 @@ else
|
||||
await ApiDenuncias.OpenGestionaFileAsync(
|
||||
fileUrl,
|
||||
createdFile.FileOpenUrl,
|
||||
managementUnitGroupId: Guid.Parse("a4ad4dfb-70dc-4219-8ee3-4dcc939f0955"),
|
||||
assignedGroupId: selectedGroup switch
|
||||
{
|
||||
"510" => Guid.Parse("6dbfc433-1eb6-4b9a-a533-bfebc652c101"),
|
||||
"600" => Guid.Parse("454fa4ec-8b82-4240-9419-113f45d4b004"),
|
||||
"700" => Guid.Parse("a4ad4dfb-70dc-4219-8ee3-4dcc939f0955"),
|
||||
_ => throw new InvalidOperationException($"Grupo desconocido: {selectedGroup}")
|
||||
},
|
||||
assignedGroupCode: selectedGroup,
|
||||
confidential: selectedDenuncias.Confidencial,
|
||||
freeTitle: nuevoAsunto,
|
||||
siaCode: "3109963"
|
||||
freeTitle: nuevoAsunto
|
||||
);
|
||||
selectedDenuncias.Expediente_Gestiona = fileUrl;
|
||||
selectedDenuncias.EnGestiona = true;
|
||||
expedienteCreadoEnGestiona = true;
|
||||
}
|
||||
|
||||
Busy.Update(message: "Guardando referencia local del expediente.", detail: "Paso 3 de 8");
|
||||
selectedDenuncias.Expediente_Gestiona = fileUrl;
|
||||
selectedDenuncias.EnGestiona = true;
|
||||
|
||||
Busy.Update(message: "Asignando el expediente al grupo elegido.", detail: "Paso 3 de 8");
|
||||
await ApiDenuncias.AssignGestionaFileAsync(fileUrl, selectedGroup);
|
||||
|
||||
var thirdParty = selectedThirdParty ?? ThirdPartyIdentityData.FromComplaint(selectedDenuncias);
|
||||
|
||||
Busy.Update(message: "Resolviendo tercero y enlazandolo al expediente.", detail: "Paso 4 de 8");
|
||||
await ApiDenuncias.EnsureGestionaThirdAndLinkAsync(fileUrl, thirdParty);
|
||||
var thirdResult = await ApiDenuncias.EnsureGestionaThirdAndLinkAsync(fileUrl, thirdParty);
|
||||
|
||||
var ahoraUtc = DateTime.UtcNow;
|
||||
var carpetaActualizacion = FixFileName($"Actualizacion {DateTime.Now:yyyy-MM-dd HH-mm-ss}");
|
||||
var carpetaActualizacion = FixFileName($"Actualizacion {DateTime.Now:yyyy-MM-dd HH-mm}");
|
||||
Busy.Update(message: "Creando carpeta de actualizacion en Gestiona.", detail: "Paso 5 de 8");
|
||||
var carpetaActualizacionGestiona = await ApiDenuncias.CreateGestionaFolderAsync(fileUrl, carpetaActualizacion);
|
||||
var documentsTargetUrl = carpetaActualizacionGestiona.DocumentsTargetUrl;
|
||||
@@ -959,8 +956,8 @@ else
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(report.FileName))
|
||||
{
|
||||
Busy.Update(message: "Preparando y subiendo el report para firma.", detail: "Paso 6 de 8");
|
||||
var reportPdfBytes = PdfHelper.MergeFilesToPdf(
|
||||
Busy.Update(message: "Preparando y subiendo el documento principal.", detail: "Paso 6 de 8");
|
||||
var reportPdfBytes = PdfHelper.MergeReportToPdf(
|
||||
new (string FileName, byte[] Content)[] { (report.FileName, report.Content) });
|
||||
var reportFinalName = FixFileName("Denuncia.pdf");
|
||||
documentoParaTramitar = await ApiDenuncias.UploadGestionaDocumentAsync(
|
||||
@@ -979,7 +976,7 @@ else
|
||||
{
|
||||
Busy.Update(message: "Uniendo adjuntos nuevos en un unico PDF y subiendolo.", detail: "Paso 7 de 8");
|
||||
var pdfBytes = PdfHelper.MergeFilesToPdf(adjuntos);
|
||||
var pdfName = FixFileName($"Adjuntos {selectedDenuncias.Id_Denuncia}_{ahoraUtc:yyyyMMddHHmmss}.pdf");
|
||||
var pdfName = BuildMergedAttachmentsFileName(selectedDenuncias.Id_Denuncia, ahoraUtc);
|
||||
var docUrl = await ApiDenuncias.UploadGestionaDocumentAsync(documentsTargetUrl, pdfBytes, pdfName);
|
||||
if (string.IsNullOrWhiteSpace(documentoParaTramitar))
|
||||
{
|
||||
@@ -1040,11 +1037,14 @@ else
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(documentoParaTramitar))
|
||||
{
|
||||
Busy.Update(message: "Enviando el report al circuito de firma.", detail: "Paso 8 de 8");
|
||||
Busy.Update(message: "Finalizando el documento principal en Gestiona.", detail: "Paso 8 de 8");
|
||||
selectedGroup = NormalizeUpdateGroup(selectedGroup);
|
||||
await ApiDenuncias.TramitarGestionaDocumentAsync(
|
||||
documentoParaTramitar,
|
||||
GetAssignedGroupLinkBySelectedGroup(),
|
||||
selectedDenuncias.Id_Denuncia);
|
||||
selectedGroup,
|
||||
selectedDenuncias.Id_Denuncia,
|
||||
isUpdate: true,
|
||||
updateSource: pendingUpdateSource);
|
||||
}
|
||||
|
||||
foreach (var orig in nombresOriginalesSubidos)
|
||||
@@ -1066,7 +1066,17 @@ else
|
||||
selectedDenuncias.EsActualizacion = false;
|
||||
selectedDenuncias.NombreDenuncia = nuevoAsunto;
|
||||
selectedDenuncias.FechaSubidaAGestiona = ahoraUtc;
|
||||
selectedDenuncias.UltimaSubidaGestionaTipo = uploadType;
|
||||
selectedDenuncias.UltimoGrupoAsignadoGestiona = GetGestionaGroupDisplay(selectedGroup);
|
||||
selectedDenuncias.PendingUpdateSource = string.Empty;
|
||||
await SincronizarExpedienteGestionaAsync(selectedDenuncias, fileUrl);
|
||||
await ActualizarDenunciaAsync(selectedDenuncias);
|
||||
var historialAviso = await RegistrarHistorialGestionaAsync(
|
||||
selectedDenuncias,
|
||||
uploadType,
|
||||
selectedGroup,
|
||||
ahoraUtc,
|
||||
string.Join("; ", nombresFinalesSubidos));
|
||||
|
||||
var denunciaProcesadaId = selectedDenuncias.Id_Denuncia;
|
||||
actualizaciones.RemoveAll(x => x.Id_Denuncia == denunciaProcesadaId);
|
||||
@@ -1076,22 +1086,35 @@ else
|
||||
var avisos = new List<string>();
|
||||
if (ficherosVacios.Count > 0)
|
||||
{
|
||||
avisos.Add($"Se omitieron ficheros vac<EFBFBD>os: {string.Join(", ", ficherosVacios)}.");
|
||||
avisos.Add($"Se omitieron ficheros vacíos: {string.Join(", ", ficherosVacios)}.");
|
||||
}
|
||||
if (ficherosNoSeleccionados.Count > 0)
|
||||
{
|
||||
avisos.Add($"No se subieron por selecci<EFBFBD>n del usuario: {string.Join(", ", ficherosNoSeleccionados)}.");
|
||||
avisos.Add($"No se subieron por selección del usuario: {string.Join(", ", ficherosNoSeleccionados)}.");
|
||||
}
|
||||
if (avisos.Count > 0)
|
||||
if (thirdResult.Warnings.Count > 0)
|
||||
{
|
||||
operationNotice = $"Actualizaci<63>n #{denunciaProcesadaId} completada. {string.Join(" ", avisos)}";
|
||||
avisos.AddRange(thirdResult.Warnings);
|
||||
}
|
||||
if (!string.IsNullOrWhiteSpace(historialAviso))
|
||||
{
|
||||
avisos.Add(historialAviso);
|
||||
}
|
||||
var expedienteInfo = string.IsNullOrWhiteSpace(selectedDenuncias.ExpedienteGestionaMostrable)
|
||||
? string.Empty
|
||||
: $" Nº expediente Gestiona: {selectedDenuncias.ExpedienteGestionaMostrable}.";
|
||||
var resumen = expedienteCreadoEnGestiona
|
||||
? $"Actualización #{denunciaProcesadaId}: se ha creado el expediente en Gestiona y se han subido los documentos.{expedienteInfo}"
|
||||
: $"Actualización #{denunciaProcesadaId}: se han añadido los documentos al expediente de Gestiona.{expedienteInfo}";
|
||||
operationNotice = avisos.Count > 0
|
||||
? $"{resumen} {string.Join(" ", avisos)}"
|
||||
: resumen;
|
||||
StateHasChanged();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.Error.WriteLine($"Error al confirmar env<EFBFBD>o (Actualizaciones): {ex}");
|
||||
operationError = $"No se ha podido completar la actualizaci<EFBFBD>n #{selectedDenuncias?.Id_Denuncia}: {ex.Message}";
|
||||
Console.Error.WriteLine($"Error al confirmar envío (Actualizaciones): {ex}");
|
||||
operationError = $"No se ha podido completar la actualización #{selectedDenuncias?.Id_Denuncia}: {ex.Message}";
|
||||
}
|
||||
finally
|
||||
{
|
||||
@@ -1186,7 +1209,7 @@ else
|
||||
}
|
||||
|
||||
private static string GetReadOnlyValue(string? value) =>
|
||||
string.IsNullOrWhiteSpace(value) ? "<EFBFBD>" : value;
|
||||
string.IsNullOrWhiteSpace(value) ? "—" : value;
|
||||
|
||||
private static bool IsExternalGestionaUpdate(DenunciasGestiona denuncia)
|
||||
{
|
||||
@@ -1264,7 +1287,7 @@ else
|
||||
private static List<FicherosDenuncias> GetPendingUpdateFiles(List<FicherosDenuncias> files, DateOnly? externalUpdateCutoffDate = null)
|
||||
{
|
||||
var uploadedHashes = files
|
||||
.Where(file => file.Subido && !file.EsReport && !string.IsNullOrWhiteSpace(file.ContentSha256))
|
||||
.Where(file => file.Subido && !string.IsNullOrWhiteSpace(file.ContentSha256))
|
||||
.Select(file => file.ContentSha256)
|
||||
.ToHashSet(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
@@ -1275,10 +1298,13 @@ else
|
||||
.Where(file => file.EsReport)
|
||||
.OrderByDescending(file => file.Fecha)
|
||||
.FirstOrDefault();
|
||||
if (report is not null)
|
||||
if (report is not null && !report.Subido)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(report.ContentSha256) || plannedHashes.Add(report.ContentSha256))
|
||||
{
|
||||
pending.Add(report);
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var file in files
|
||||
.Where(file => !file.EsReport && !file.Subido)
|
||||
@@ -1341,15 +1367,80 @@ else
|
||||
: "Sin historico local";
|
||||
}
|
||||
|
||||
private string GetAssignedGroupLinkBySelectedGroup()
|
||||
private static string NormalizeUpdateGroup(string? groupCode)
|
||||
=> groupCode?.Trim().StartsWith("510", StringComparison.Ordinal) == true
|
||||
? "510"
|
||||
: "600";
|
||||
|
||||
private static string GetGestionaGroupDisplay(string? groupCode)
|
||||
{
|
||||
return selectedGroup switch
|
||||
return string.IsNullOrWhiteSpace(groupCode) ? string.Empty : groupCode.Trim();
|
||||
}
|
||||
|
||||
private static string GetUpdateUploadType(string? updateSource, string groupCode)
|
||||
{
|
||||
"510" => "https://02.g3stiona.com/rest/groups/6dbfc433-1eb6-4b9a-a533-bfebc652c101",
|
||||
"600" => "https://02.g3stiona.com/rest/groups/454fa4ec-8b82-4240-9419-113f45d4b004",
|
||||
"700" => "https://02.g3stiona.com/rest/groups/a4ad4dfb-70dc-4219-8ee3-4dcc939f0955",
|
||||
_ => throw new InvalidOperationException($"Grupo desconocido: {selectedGroup}")
|
||||
};
|
||||
if (!ComplaintUpdateSources.IsReceiver(updateSource))
|
||||
{
|
||||
return "Actualización";
|
||||
}
|
||||
|
||||
return NormalizeUpdateGroup(groupCode) == "510"
|
||||
? "Comunic. SDI a denunciante"
|
||||
: "Comunic. SAJ a denunciante";
|
||||
}
|
||||
|
||||
private async Task<bool> ConfirmSelectedGroupAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
var currentGroups = await ApiDenuncias.GetCurrentUserWorkGroupsAsync();
|
||||
if (currentGroups.GroupCodes.Contains(selectedGroup, StringComparer.OrdinalIgnoreCase))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
var configuredGroups = currentGroups.GroupCodes.Count == 0
|
||||
? "ningún grupo"
|
||||
: string.Join(", ", currentGroups.GroupCodes);
|
||||
return await Dialogs.ConfirmAsync(
|
||||
"Cambio de asignación en Gestiona",
|
||||
$"El grupo de destino {selectedGroup} no pertenece a tus grupos configurados ({configuredGroups}). " +
|
||||
"Si continúas, la denuncia cambiará su asignación en Gestiona a un grupo distinto de los tuyos.",
|
||||
"Cambiar asignación");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
operationError = $"No se han podido comprobar tus grupos antes de la subida: {ex.Message}";
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<string?> RegistrarHistorialGestionaAsync(
|
||||
DenunciasGestiona denuncia,
|
||||
string tipoSubida,
|
||||
string grupo,
|
||||
DateTime uploadedAtUtc,
|
||||
string documentos)
|
||||
{
|
||||
try
|
||||
{
|
||||
await ApiDenuncias.RegisterGestionaUploadHistoryAsync(
|
||||
new GestionaUploadHistoryCreateRequest(
|
||||
denuncia.Id_Denuncia,
|
||||
denuncia.Expediente_Gestiona ?? string.Empty,
|
||||
denuncia.ExpedienteGestionaMostrable,
|
||||
tipoSubida,
|
||||
grupo,
|
||||
uploadedAtUtc,
|
||||
denuncia.NombreDenuncia ?? string.Empty,
|
||||
documentos));
|
||||
return null;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.Error.WriteLine($"No se ha podido registrar el historico de subida a Gestiona: {ex}");
|
||||
return "La subida se ha completado, pero no se ha podido registrar el histórico operativo en la aplicación.";
|
||||
}
|
||||
}
|
||||
|
||||
private async Task SincronizarExpedienteGestionaAsync(DenunciasGestiona denuncia, string fileUrl)
|
||||
@@ -1385,3 +1476,4 @@ else
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -144,9 +144,9 @@
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Expediente</th>
|
||||
<th>Asunto</th>
|
||||
<th>T<EFBFBD>tulo expediente</th>
|
||||
<th>Fecha creaci<63>n</th>
|
||||
<th>Estado</th>
|
||||
<th><EFBFBD>ltima actividad Gestiona</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
@@ -157,11 +157,11 @@
|
||||
<td>@exp.CodigoExpediente</td>
|
||||
<td>@exp.Asunto</td>
|
||||
<td>@(exp.FechaCreacion?.ToLocalTime().ToString("dd/MM/yyyy HH:mm"))</td>
|
||||
<td>@exp.Estado</td>
|
||||
<td>@(exp.FechaUltimaModificacion?.ToLocalTime().ToString("dd/MM/yyyy HH:mm") ?? "-")</td>
|
||||
<td class="text-end">
|
||||
<button class="btn btn-sm btn-outline-primary"
|
||||
@onclick="() => ToggleDetalle(exp)">
|
||||
@(expedienteSeleccionado == exp ? "Ocultar" : "Abrir expediente")
|
||||
@(expedienteSeleccionado == exp ? "Ocultar" : "Consultar expediente Gestiona")
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
@@ -172,26 +172,30 @@
|
||||
<td colspan="5">
|
||||
<dl class="row mb-0">
|
||||
<dt class="col-sm-2">Expediente</dt>
|
||||
<dd class="col-sm-10">@exp.CodigoExpediente</dd>
|
||||
<dd class="col-sm-10">@FormatDetailValue(exp.CodigoExpediente)</dd>
|
||||
|
||||
<dt class="col-sm-2">Asunto</dt>
|
||||
<dd class="col-sm-10">@exp.Asunto</dd>
|
||||
<dt class="col-sm-2">T<EFBFBD>tulo expediente</dt>
|
||||
<dd class="col-sm-10">@FormatDetailValue(exp.Asunto)</dd>
|
||||
|
||||
<dt class="col-sm-2">Procedimiento</dt>
|
||||
<dd class="col-sm-10">@FormatDetailValue(exp.Procedimiento)</dd>
|
||||
|
||||
<dt class="col-sm-2">Fecha creaci<63>n</dt>
|
||||
<dd class="col-sm-10">
|
||||
@exp.FechaCreacion?.ToLocalTime().ToString("dd/MM/yyyy HH:mm")
|
||||
@(exp.FechaCreacion?.ToLocalTime().ToString("dd/MM/yyyy HH:mm") ?? "-")
|
||||
</dd>
|
||||
|
||||
<dt class="col-sm-2">Estado</dt>
|
||||
<dd class="col-sm-10">@exp.Estado</dd>
|
||||
|
||||
@if (!string.IsNullOrWhiteSpace(exp.FileUrl))
|
||||
{
|
||||
<dt class="col-sm-2">Enlace Gestiona</dt>
|
||||
<dt class="col-sm-2"><EFBFBD>ltima actividad en Gestiona</dt>
|
||||
<dd class="col-sm-10">
|
||||
<a href="@exp.FileUrl" target="_blank">@exp.FileUrl</a>
|
||||
@(exp.FechaUltimaModificacion?.ToLocalTime().ToString("dd/MM/yyyy HH:mm") ?? "-")
|
||||
</dd>
|
||||
|
||||
@if (!string.IsNullOrWhiteSpace(exp.UltimaAuditoriaMensaje))
|
||||
{
|
||||
<dt class="col-sm-2"><3E>ltima acci<63>n</dt>
|
||||
<dd class="col-sm-10">@exp.UltimaAuditoriaMensaje</dd>
|
||||
}
|
||||
|
||||
</dl>
|
||||
</td>
|
||||
</tr>
|
||||
@@ -233,6 +237,8 @@
|
||||
|
||||
private bool IsModo(string valor) => string.Equals(modoFecha, valor, StringComparison.Ordinal);
|
||||
private void SetModo(string valor) => modoFecha = valor;
|
||||
private static string FormatDetailValue(string? value)
|
||||
=> string.IsNullOrWhiteSpace(value) ? "-" : value;
|
||||
|
||||
private void ToggleDetalle(ExpedienteTerceroDto exp)
|
||||
{
|
||||
|
||||
@@ -4,14 +4,35 @@
|
||||
|
||||
@using System.Globalization
|
||||
@using GestionaDenunciasAN.Services
|
||||
@using GestionaDenuncias.Shared.Models
|
||||
@using Microsoft.AspNetCore.Components.Authorization
|
||||
|
||||
@inject ApiDenunciasClient ApiDenuncias
|
||||
@inject AuthenticationStateProvider AuthenticationStateProvider
|
||||
@inject UiBusyService Busy
|
||||
|
||||
<PageTitle>Configuracion</PageTitle>
|
||||
|
||||
<h3>Configuracion</h3>
|
||||
|
||||
@if (!hasCheckedConfigurationAccess)
|
||||
{
|
||||
<div class="card mt-3">
|
||||
<div class="card-body">
|
||||
<span class="spinner-border spinner-border-sm me-2" role="status" aria-hidden="true"></span>
|
||||
Cargando configuracion...
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
else if (!hasConfigurationAccess)
|
||||
{
|
||||
<div class="alert alert-warning mt-3">
|
||||
<strong>Configuracion restringida.</strong>
|
||||
<div>Esta pantalla solo esta disponible para los usuarios autorizados del sistema.</div>
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="card mt-3">
|
||||
<div class="card-body">
|
||||
<h5 class="mb-3">Fecha de actualizaciones externas</h5>
|
||||
@@ -75,10 +96,123 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card mt-3">
|
||||
<div class="card-body">
|
||||
<div class="d-flex flex-wrap justify-content-between align-items-center gap-2 mb-3">
|
||||
<div>
|
||||
<h5 class="mb-1">Grupos de trabajo</h5>
|
||||
<p class="text-muted mb-0">
|
||||
Los usuarios pueden consultar y tratar denuncias de otros propietarios cuando comparten al menos un grupo.
|
||||
</p>
|
||||
</div>
|
||||
<button type="button"
|
||||
class="btn btn-outline-secondary btn-sm"
|
||||
disabled="@isLoadingWorkGroups"
|
||||
@onclick="LoadWorkGroupsAsync">
|
||||
Actualizar usuarios
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@if (isLoadingWorkGroups)
|
||||
{
|
||||
<div class="text-muted">
|
||||
<span class="spinner-border spinner-border-sm me-2" role="status" aria-hidden="true"></span>
|
||||
Cargando usuarios y grupos...
|
||||
</div>
|
||||
}
|
||||
else if (workGroupAdministration is null)
|
||||
{
|
||||
<div class="alert alert-warning mb-0">No se ha podido cargar la configuracion de grupos.</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="table-responsive">
|
||||
<table class="table table-sm align-middle mb-0">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Usuario</th>
|
||||
@foreach (var group in workGroupAdministration.Groups)
|
||||
{
|
||||
<th>
|
||||
<span class="d-block">@group.Code</span>
|
||||
<small class="text-muted fw-normal">@group.Name</small>
|
||||
</th>
|
||||
}
|
||||
<th class="text-end">Accion</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach (var user in workGroupAdministration.Users)
|
||||
{
|
||||
<tr @key="user.UserId">
|
||||
<td>
|
||||
<strong>@user.Username</strong>
|
||||
@if (!GetSelectedGroups(user.UserId).Any())
|
||||
{
|
||||
<span class="badge text-bg-warning ms-2">Sin grupo</span>
|
||||
}
|
||||
</td>
|
||||
@foreach (var group in workGroupAdministration.Groups)
|
||||
{
|
||||
var checkboxId = $"user-group-{user.UserId}-{group.Code}";
|
||||
<td>
|
||||
<div class="form-check">
|
||||
<input id="@checkboxId"
|
||||
class="form-check-input"
|
||||
type="checkbox"
|
||||
checked="@GetSelectedGroups(user.UserId).Contains(group.Code)"
|
||||
disabled="@savingWorkGroupUsers.Contains(user.UserId)"
|
||||
@onchange="args => ToggleUserGroup(user.UserId, group.Code, args)" />
|
||||
<label class="form-check-label" for="@checkboxId">@group.Code</label>
|
||||
</div>
|
||||
</td>
|
||||
}
|
||||
<td class="text-end">
|
||||
<button type="button"
|
||||
class="btn btn-primary btn-sm"
|
||||
disabled="@savingWorkGroupUsers.Contains(user.UserId)"
|
||||
@onclick="() => SaveUserGroupsAsync(user)">
|
||||
@(savingWorkGroupUsers.Contains(user.UserId) ? "Guardando..." : "Guardar")
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (!string.IsNullOrWhiteSpace(workGroupNotice))
|
||||
{
|
||||
<div class="alert alert-success mt-3 mb-0">@workGroupNotice</div>
|
||||
}
|
||||
|
||||
@if (!string.IsNullOrWhiteSpace(workGroupError))
|
||||
{
|
||||
<div class="alert alert-danger mt-3 mb-0">@workGroupError</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card mt-3">
|
||||
<div class="card-body">
|
||||
<h5 class="mb-3">Purga manual con reemplazo</h5>
|
||||
|
||||
<div class="border rounded px-3 py-2 bg-light mb-3">
|
||||
<div class="d-flex flex-wrap align-items-center gap-2">
|
||||
<span class="bi bi-key-fill text-primary" aria-hidden="true"></span>
|
||||
<strong>Ultima clave diaria registrada</strong>
|
||||
<span class="text-muted">@(string.IsNullOrWhiteSpace(latestEncryptionKeyDate) ? "No disponible" : latestEncryptionKeyDate)</span>
|
||||
@if (!string.IsNullOrWhiteSpace(latestEncryptionKeyStatus))
|
||||
{
|
||||
<span class="badge rounded-pill text-bg-secondary">@latestEncryptionKeyStatus</span>
|
||||
}
|
||||
</div>
|
||||
<div class="form-text mt-1">
|
||||
Es la fecha de la fila mas reciente en <code>encryption_keys</code>; sirve para comprobar rapidamente que la rotacion diaria esta llegando a la base de datos.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="alert alert-danger">
|
||||
<div class="d-flex gap-3">
|
||||
<span class="bi bi-exclamation-triangle-fill fs-3" aria-hidden="true"></span>
|
||||
@@ -169,14 +303,31 @@
|
||||
<div>@purgeErrorMessage</div>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
@code {
|
||||
private const string RequiredConfirmation = "PURGAR CLAVE ACTUAL";
|
||||
private static readonly HashSet<string> ConfigurationUsers = new(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
"pcornejo",
|
||||
"Rgarciaglbk",
|
||||
"eaguilarGestor"
|
||||
};
|
||||
|
||||
private bool hasCheckedConfigurationAccess;
|
||||
private bool hasConfigurationAccess;
|
||||
private string externalUpdateCutoffDate = string.Empty;
|
||||
private string latestEncryptionKeyDate = string.Empty;
|
||||
private string latestEncryptionKeyStatus = string.Empty;
|
||||
private string? configurationNotice;
|
||||
private string? configurationError;
|
||||
private bool isSavingConfiguration;
|
||||
private WorkGroupAdministrationDto? workGroupAdministration;
|
||||
private readonly Dictionary<long, HashSet<string>> selectedWorkGroups = [];
|
||||
private readonly HashSet<long> savingWorkGroupUsers = [];
|
||||
private bool isLoadingWorkGroups;
|
||||
private string? workGroupNotice;
|
||||
private string? workGroupError;
|
||||
|
||||
private string confirmation = string.Empty;
|
||||
private bool acceptedRisk;
|
||||
@@ -191,7 +342,16 @@
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
var authState = await AuthenticationStateProvider.GetAuthenticationStateAsync();
|
||||
hasConfigurationAccess = IsConfigurationUser(authState.User.Identity?.Name);
|
||||
hasCheckedConfigurationAccess = true;
|
||||
if (!hasConfigurationAccess)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
await LoadConfigurationAsync();
|
||||
await LoadWorkGroupsAsync();
|
||||
}
|
||||
|
||||
private async Task LoadConfigurationAsync()
|
||||
@@ -201,6 +361,8 @@
|
||||
configurationError = null;
|
||||
var config = await ApiDenuncias.GetAppConfigurationAsync();
|
||||
externalUpdateCutoffDate = ToSpanishDateText(config.ExternalUpdateCutoffDate);
|
||||
latestEncryptionKeyDate = ToSpanishDateText(config.LatestEncryptionKeyDate);
|
||||
latestEncryptionKeyStatus = ToSpanishKeyStatus(config.LatestEncryptionKeyStatus);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
@@ -219,6 +381,8 @@
|
||||
|
||||
var config = await ApiDenuncias.UpdateExternalUpdateCutoffDateAsync(date);
|
||||
externalUpdateCutoffDate = ToSpanishDateText(config.ExternalUpdateCutoffDate);
|
||||
latestEncryptionKeyDate = ToSpanishDateText(config.LatestEncryptionKeyDate);
|
||||
latestEncryptionKeyStatus = ToSpanishKeyStatus(config.LatestEncryptionKeyStatus);
|
||||
configurationNotice = string.IsNullOrWhiteSpace(externalUpdateCutoffDate)
|
||||
? "Fecha de corte eliminada."
|
||||
: $"Fecha de corte guardada: {externalUpdateCutoffDate}.";
|
||||
@@ -239,6 +403,98 @@
|
||||
await SaveExternalUpdateCutoffDateAsync();
|
||||
}
|
||||
|
||||
private async Task LoadWorkGroupsAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
isLoadingWorkGroups = true;
|
||||
workGroupError = null;
|
||||
var response = await ApiDenuncias.GetWorkGroupAdministrationAsync();
|
||||
ApplyWorkGroupAdministration(response);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
workGroupError = $"No se han podido cargar los grupos de trabajo: {ex.Message}";
|
||||
}
|
||||
finally
|
||||
{
|
||||
isLoadingWorkGroups = false;
|
||||
}
|
||||
}
|
||||
|
||||
private void ApplyWorkGroupAdministration(WorkGroupAdministrationDto response)
|
||||
{
|
||||
workGroupAdministration = response;
|
||||
selectedWorkGroups.Clear();
|
||||
foreach (var user in response.Users)
|
||||
{
|
||||
selectedWorkGroups[user.UserId] = user.GroupCodes.ToHashSet(
|
||||
StringComparer.OrdinalIgnoreCase);
|
||||
}
|
||||
}
|
||||
|
||||
private HashSet<string> GetSelectedGroups(long userId)
|
||||
{
|
||||
if (!selectedWorkGroups.TryGetValue(userId, out var groups))
|
||||
{
|
||||
groups = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||
selectedWorkGroups[userId] = groups;
|
||||
}
|
||||
|
||||
return groups;
|
||||
}
|
||||
|
||||
private void ToggleUserGroup(
|
||||
long userId,
|
||||
string groupCode,
|
||||
ChangeEventArgs args)
|
||||
{
|
||||
var groups = GetSelectedGroups(userId);
|
||||
var enabled = args.Value is bool boolValue
|
||||
? boolValue
|
||||
: bool.TryParse(args.Value?.ToString(), out var parsed) && parsed;
|
||||
|
||||
if (enabled)
|
||||
{
|
||||
groups.Add(groupCode);
|
||||
}
|
||||
else
|
||||
{
|
||||
groups.Remove(groupCode);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task SaveUserGroupsAsync(UserWorkGroupDto user)
|
||||
{
|
||||
var groups = GetSelectedGroups(user.UserId);
|
||||
if (groups.Count == 0)
|
||||
{
|
||||
workGroupNotice = null;
|
||||
workGroupError = $"El usuario {user.Username} debe pertenecer al menos a un grupo.";
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
savingWorkGroupUsers.Add(user.UserId);
|
||||
workGroupNotice = null;
|
||||
workGroupError = null;
|
||||
var response = await ApiDenuncias.UpdateUserWorkGroupsAsync(
|
||||
user.Username,
|
||||
groups.OrderBy(code => code, StringComparer.Ordinal).ToArray());
|
||||
ApplyWorkGroupAdministration(response);
|
||||
workGroupNotice = $"Grupos de {user.Username} actualizados correctamente.";
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
workGroupError = $"No se han podido guardar los grupos de {user.Username}: {ex.Message}";
|
||||
}
|
||||
finally
|
||||
{
|
||||
savingWorkGroupUsers.Remove(user.UserId);
|
||||
}
|
||||
}
|
||||
|
||||
private static string? ToIsoDateText(string? value)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(value))
|
||||
@@ -287,6 +543,25 @@
|
||||
: value.Trim();
|
||||
}
|
||||
|
||||
private static string ToSpanishKeyStatus(string? status)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(status))
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
return status.Trim().ToLowerInvariant() switch
|
||||
{
|
||||
"active" => "activa",
|
||||
"purged" => "purgada",
|
||||
_ => status.Trim()
|
||||
};
|
||||
}
|
||||
|
||||
private static bool IsConfigurationUser(string? username)
|
||||
=> !string.IsNullOrWhiteSpace(username) &&
|
||||
ConfigurationUsers.Contains(username.Trim());
|
||||
|
||||
private async Task ExecutePurgeAsync()
|
||||
{
|
||||
if (!CanExecutePurge)
|
||||
@@ -305,6 +580,7 @@
|
||||
try
|
||||
{
|
||||
purgeResult = await ApiDenuncias.ExecuteCurrentManualPurgeAsync();
|
||||
await LoadConfigurationAsync();
|
||||
acceptedRisk = false;
|
||||
confirmation = string.Empty;
|
||||
}
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
@page "/GestionZip"
|
||||
@page "/Entrada"
|
||||
@rendermode @(new InteractiveServerRenderMode(prerender: false))
|
||||
@attribute [Authorize]
|
||||
@implements IAsyncDisposable
|
||||
@using System.Globalization
|
||||
@using System.Text.RegularExpressions
|
||||
@using GestionaDenunciasAN.Models
|
||||
@inject AuthenticationStateProvider AuthenticationStateProvider
|
||||
@inject ApiDenunciasClient ApiDenuncias
|
||||
@inject IJSRuntime JSRuntime
|
||||
@inject UiBusyService Busy
|
||||
@inject UiDialogService Dialogs
|
||||
|
||||
<PageTitle>Entrada de denuncias</PageTitle>
|
||||
|
||||
@@ -73,9 +75,84 @@
|
||||
.report-detail-close {
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
.report-row-disabled {
|
||||
opacity: 0.62;
|
||||
}
|
||||
|
||||
.report-row-disabled td {
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.inbox-table-wrap {
|
||||
overflow-x: auto;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
}
|
||||
|
||||
.inbox-table {
|
||||
min-width: 1080px;
|
||||
font-size: 0.84rem;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.inbox-table th,
|
||||
.inbox-table td {
|
||||
vertical-align: middle;
|
||||
padding: 0.45rem 0.5rem;
|
||||
}
|
||||
|
||||
.inbox-report-cell {
|
||||
width: 4.5rem;
|
||||
}
|
||||
|
||||
.inbox-channel-cell {
|
||||
width: 10.5rem;
|
||||
max-width: 10.5rem;
|
||||
}
|
||||
|
||||
.inbox-channel-cell .text-truncate {
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.inbox-tracking-cell {
|
||||
width: 11rem;
|
||||
max-width: 11rem;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.inbox-activity-cell {
|
||||
width: 9.25rem;
|
||||
max-width: 12rem;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.inbox-action-cell {
|
||||
width: 5.5rem;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
|
||||
.inbox-detail-button {
|
||||
padding-inline: 0.55rem;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
@@media (max-width: 768px) {
|
||||
.inbox-table {
|
||||
min-width: 1020px;
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
.inbox-table th,
|
||||
.inbox-table td {
|
||||
padding: 0.4rem 0.42rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="container py-4">
|
||||
<div class="container-fluid py-4 px-3 px-xl-4">
|
||||
<div class="d-flex flex-column flex-lg-row justify-content-between align-items-lg-center gap-3 mb-4">
|
||||
<div>
|
||||
<h3 class="mb-1">Entrada de denuncias</h3>
|
||||
@@ -96,26 +173,25 @@
|
||||
<div class="alert @FilterWarningCss mb-4">@FilterWarningMessage</div>
|
||||
}
|
||||
|
||||
<div class="row g-4">
|
||||
<div class="col-lg-4">
|
||||
<div class="card shadow-sm h-100">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">Sesion GlobalLeaks</h5>
|
||||
<p class="mb-2"><strong>Usuario:</strong> @CurrentUsername</p>
|
||||
<p class="mb-2"><strong>Estado:</strong> @SessionStatusText</p>
|
||||
<p class="mb-2">
|
||||
<strong>Ultima descarga registrada:</strong>
|
||||
<div class="card shadow-sm mb-3">
|
||||
<div class="card-body py-3">
|
||||
<div class="row g-3 align-items-end">
|
||||
<div class="col-xl-5 col-lg-6">
|
||||
<h5 class="card-title mb-2">Sesion GlobalLeaks</h5>
|
||||
<div class="d-flex flex-wrap gap-3 small">
|
||||
<span><strong>Usuario:</strong> @CurrentUsername</span>
|
||||
<span><strong>Estado:</strong> @SessionStatusText</span>
|
||||
<span>
|
||||
<strong>Ultima descarga:</strong>
|
||||
@(UserInboxState.LastDownloadedReportMomentUtc is null
|
||||
? "Sin descargas previas"
|
||||
: UserInboxState.LastDownloadedReportMomentUtc.Value.ToLocalTime().ToString("dd/MM/yyyy HH:mm"))
|
||||
</p>
|
||||
<p class="text-muted small mb-4">
|
||||
La base de datos lleva la cuenta de lo que ya ha descargado este usuario, de lo que han descargado otros
|
||||
y de si el expediente ya esta creado en Gestiona.
|
||||
</p>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Nuevo codigo 2FA</label>
|
||||
<div class="col-xl-5 col-lg-4">
|
||||
<label class="form-label mb-1">Nuevo codigo 2FA</label>
|
||||
<input class="form-control"
|
||||
@bind="RenewAuthcode"
|
||||
maxlength="6"
|
||||
@@ -124,14 +200,15 @@
|
||||
placeholder="123456" />
|
||||
</div>
|
||||
|
||||
<div class="col-xl-2 col-lg-2">
|
||||
<button type="button" class="btn btn-primary w-100" @onclick="RenewSessionAsync" disabled="@RenewBusy">
|
||||
@(RenewBusy ? SessionRenewBusyText : SessionRenewButtonText)
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-lg-8">
|
||||
<div class="card shadow-sm">
|
||||
<div class="card-body">
|
||||
<div class="d-flex flex-column flex-md-row justify-content-between align-items-md-center gap-3 mb-3">
|
||||
@@ -152,7 +229,8 @@
|
||||
<select class="form-select" value="@Filter" @onchange="OnFilterChanged">
|
||||
<option value="all">Todas</option>
|
||||
<option value="new">Nuevas / sin leer</option>
|
||||
<option value="updated">Actualizaciones</option>
|
||||
<option value="updated">Actualizaciones del ciudadano</option>
|
||||
<option value="receiver">Actividad OAAF</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
@@ -206,6 +284,12 @@
|
||||
<div class="d-flex flex-column flex-md-row justify-content-between align-items-md-center gap-3 mb-3">
|
||||
<div class="text-muted">
|
||||
@VisibleReports.Count denuncia(s) visibles, @SelectedReportsCount seleccionada(s).
|
||||
@if (VisibleReports.Any(report => report.Accessible == false))
|
||||
{
|
||||
<span class="d-block small text-danger">
|
||||
@VisibleReports.Count(report => report.Accessible == false) denuncia(s) marcadas por GlobalLeaks como no accesibles para este usuario.
|
||||
</span>
|
||||
}
|
||||
@if (UserInboxState.LastDownloadedReportMomentUtc is not null)
|
||||
{
|
||||
<span class="d-block small">
|
||||
@@ -214,7 +298,7 @@
|
||||
}
|
||||
</div>
|
||||
<div class="d-flex gap-2">
|
||||
<button type="button" class="btn btn-outline-secondary btn-sm" @onclick="ToggleSelectAll" disabled="@(!VisibleReports.Any())">
|
||||
<button type="button" class="btn btn-outline-secondary btn-sm" @onclick="ToggleSelectAll" disabled="@(!VisibleReports.Any(CanUseReport))">
|
||||
@SelectAllLabel
|
||||
</button>
|
||||
<button type="button" class="btn btn-success btn-sm" @onclick="ImportSelectedAsync" disabled="@(!CanImportSelected)">
|
||||
@@ -223,25 +307,27 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover align-middle">
|
||||
<div class="table-responsive inbox-table-wrap">
|
||||
<table class="table table-hover table-sm align-middle inbox-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width: 3rem;"></th>
|
||||
<th>#</th>
|
||||
<th>Canal</th>
|
||||
<th>Presentacion</th>
|
||||
<th>Ultima actualizacion</th>
|
||||
<th>Estado</th>
|
||||
<th>Seguimiento</th>
|
||||
<th style="width: 7rem;">Detalle</th>
|
||||
<th title="Fecha de la última aportación o comentario realizado por el ciudadano.">Actividad ciudadano</th>
|
||||
<th title="Fecha de la última comunicación o fichero incorporado por un gestor de la OAAF.">Actividad OAAF</th>
|
||||
<th title="Resume si la denuncia es nueva, tiene cambios pendientes o ya está actualizada en Gestiona.">Estado</th>
|
||||
<th title="Indica si tu usuario gestor del buzón puede acceder a esta denuncia.">Acceso</th>
|
||||
<th title="Indica si el cambio está pendiente, descargado en la aplicación o ya incorporado a Gestiona.">Gestiona</th>
|
||||
<th class="inbox-action-cell" title="Abre el detalle disponible en GlobalLeaks.">Detalle</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@if (!CanUseGlobalLeaks)
|
||||
{
|
||||
<tr>
|
||||
<td colspan="8" class="text-muted">
|
||||
<td colspan="10" class="text-muted">
|
||||
Renueva la sesion de GlobalLeaks con un 2FA valido para cargar la bandeja.
|
||||
</td>
|
||||
</tr>
|
||||
@@ -249,13 +335,13 @@
|
||||
else if (ReportsBusy)
|
||||
{
|
||||
<tr>
|
||||
<td colspan="8" class="text-muted">Cargando denuncias...</td>
|
||||
<td colspan="10" class="text-muted">Cargando denuncias...</td>
|
||||
</tr>
|
||||
}
|
||||
else if (!VisibleReports.Any())
|
||||
{
|
||||
<tr>
|
||||
<td colspan="8" class="text-muted">No hay denuncias con los filtros actuales.</td>
|
||||
<td colspan="10" class="text-muted">No hay denuncias con los filtros actuales.</td>
|
||||
</tr>
|
||||
}
|
||||
else
|
||||
@@ -266,33 +352,35 @@
|
||||
<td>
|
||||
<input type="checkbox"
|
||||
checked="@SelectedIds.Contains(report.Id)"
|
||||
@onchange="@((ChangeEventArgs args) => ToggleSelection(report.Id, args))" />
|
||||
disabled="@(!CanUseReport(report) || ImportBusy)"
|
||||
title="@GetReportActionBlockReason(report)"
|
||||
@onchange="@((ChangeEventArgs args) => ToggleSelection(report, args))" />
|
||||
</td>
|
||||
<td><strong>#@(report.Progressive ?? 0)</strong></td>
|
||||
<td>@(report.ContextName ?? report.ContextId ?? "-")</td>
|
||||
<td class="inbox-report-cell"><strong>#@(report.Progressive ?? 0)</strong></td>
|
||||
<td class="inbox-channel-cell" title="@(report.ContextName ?? report.ContextId ?? string.Empty)"><span class="d-inline-block text-truncate">@(report.ContextName ?? report.ContextId ?? "-")</span></td>
|
||||
<td>@FormatDate(report.CreationDate)</td>
|
||||
<td>@FormatDate(report.UpdateDate)</td>
|
||||
<td class="inbox-activity-cell">@FormatCitizenActivity(report)</td>
|
||||
<td class="inbox-activity-cell" title="@GetReceiverActivityTitle(report)">@FormatReceiverActivity(report)</td>
|
||||
<td>
|
||||
<span class="badge @GetStatusBadgeCss(report)">
|
||||
<span class="badge @GetStatusBadgeCss(report)" title="@GetStatusHelp(report)">
|
||||
@GetStatusLabel(report)
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
<span class="badge @GetTrackingBadgeCss(report)">
|
||||
@GetTrackingLabel(report)
|
||||
<span class="badge @GetAccessBadgeCss(report)" title="@GetAccessHelp(report)">
|
||||
@GetAccessLabel(report)
|
||||
</span>
|
||||
@if (!string.IsNullOrWhiteSpace(report.TrackingNote))
|
||||
{
|
||||
<div class="small text-muted mt-1">@report.TrackingNote</div>
|
||||
}
|
||||
</td>
|
||||
<td>
|
||||
<td class="inbox-tracking-cell">
|
||||
<span class="badge @GetTrackingBadgeCss(report)" title="@GetTrackingHelp(report)">@GetTrackingLabel(report)</span>
|
||||
</td>
|
||||
<td class="inbox-action-cell">
|
||||
<button type="button"
|
||||
class="btn btn-outline-secondary btn-sm"
|
||||
title="Consulta mensajes y ficheros. GlobalLeaks puede marcar la denuncia como leida."
|
||||
class="btn btn-outline-secondary btn-sm inbox-detail-button"
|
||||
title="@GetReportDetailTitle(report)"
|
||||
@onclick="@(() => OpenReportDetailAsync(report))"
|
||||
disabled="@DetailBusy">
|
||||
Ver detalle
|
||||
disabled="@(DetailBusy || report.Accessible == false)">
|
||||
Detalle
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
@@ -304,8 +392,6 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if (DetailModalVisible)
|
||||
{
|
||||
@@ -351,7 +437,7 @@
|
||||
{
|
||||
<div class="border rounded p-3 mb-2 @(comment.IsNew ? "border-success bg-success-subtle" : "bg-light")">
|
||||
<div class="d-flex flex-column flex-md-row justify-content-between gap-2 small text-muted mb-2">
|
||||
<strong>@GetCommentAuthorLabel(comment.Type)</strong>
|
||||
<strong>@GetCommentAuthorLabel(comment)</strong>
|
||||
<span>@FormatDate(comment.CreationDate)</span>
|
||||
</div>
|
||||
@if (comment.IsNew)
|
||||
@@ -401,6 +487,10 @@
|
||||
<span class="small text-muted">@FormatDate(file.CreationDate)</span>
|
||||
</div>
|
||||
<div class="small text-muted">@FormatBytes(file.Size) @(string.IsNullOrWhiteSpace(file.ContentType) ? string.Empty : $" - {file.ContentType}")</div>
|
||||
@if (!string.IsNullOrWhiteSpace(file.AuthorName))
|
||||
{
|
||||
<div class="small text-muted">Añadido por @file.AuthorName</div>
|
||||
}
|
||||
@if (file.IsNew)
|
||||
{
|
||||
<span class="badge bg-success mt-2">Nuevo</span>
|
||||
@@ -451,7 +541,7 @@
|
||||
|
||||
private bool CanUseGlobalLeaks => SessionInfo?.HasActiveSession == true;
|
||||
private bool RenewPrepared => !string.IsNullOrWhiteSpace(RenewPendingLoginId);
|
||||
private int SelectedReportsCount => SelectedIds.Count;
|
||||
private int SelectedReportsCount => Reports.Count(report => SelectedIds.Contains(report.Id) && CanUseReport(report));
|
||||
private bool CanImportSelected => CanUseGlobalLeaks && SelectedReportsCount > 0 && !ImportBusy;
|
||||
private string SessionStatusText => SessionInfo is null
|
||||
? "Sin credenciales guardadas"
|
||||
@@ -464,7 +554,7 @@
|
||||
private string SessionRenewBusyText => RenewPrepared
|
||||
? "Validando 2FA..."
|
||||
: "Preparando...";
|
||||
private string SelectAllLabel => VisibleReports.Count > 0 && VisibleReports.All(report => SelectedIds.Contains(report.Id))
|
||||
private string SelectAllLabel => VisibleReports.Any(CanUseReport) && VisibleReports.Where(CanUseReport).All(report => SelectedIds.Contains(report.Id))
|
||||
? "Deseleccionar todas"
|
||||
: "Seleccionar todas";
|
||||
|
||||
@@ -622,6 +712,37 @@
|
||||
return;
|
||||
}
|
||||
|
||||
var selectedReports = Reports
|
||||
.Where(report => SelectedIds.Contains(report.Id))
|
||||
.Where(CanUseReport)
|
||||
.OrderBy(report => report.Progressive ?? 0)
|
||||
.ToList();
|
||||
|
||||
if (selectedReports.Count == 0)
|
||||
{
|
||||
SetStatus("No hay denuncias accesibles seleccionadas para importar.", "alert-warning");
|
||||
return;
|
||||
}
|
||||
|
||||
var reportsFromOtherOwners = selectedReports
|
||||
.Where(report => report.RequiresOwnerConfirmation)
|
||||
.ToArray();
|
||||
if (reportsFromOtherOwners.Length > 0)
|
||||
{
|
||||
var ownerSummary = string.Join(
|
||||
Environment.NewLine,
|
||||
reportsFromOtherOwners.Select(report =>
|
||||
$"Denuncia #{report.Progressive ?? 0}: propiedad de {report.OwnerUsername}."));
|
||||
var confirmed = await Dialogs.ConfirmAsync(
|
||||
"Denuncias asignadas a otros usuarios",
|
||||
$"Vas a importar denuncias asignadas a otros usuarios de tu grupo:{Environment.NewLine}{Environment.NewLine}{ownerSummary}{Environment.NewLine}{Environment.NewLine}La importación quedará registrada con tu usuario.",
|
||||
"Importar denuncias");
|
||||
if (!confirmed)
|
||||
{
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
ImportBusy = true;
|
||||
var importedCount = 0;
|
||||
var errors = new List<string>();
|
||||
@@ -629,11 +750,6 @@
|
||||
|
||||
try
|
||||
{
|
||||
var selectedReports = Reports
|
||||
.Where(report => SelectedIds.Contains(report.Id))
|
||||
.OrderBy(report => report.Progressive ?? 0)
|
||||
.ToList();
|
||||
|
||||
using var busy = Busy.Show(
|
||||
"Importando denuncias",
|
||||
$"Descargando y procesando {selectedReports.Count} denuncia(s) desde GlobalLeaks.",
|
||||
@@ -651,7 +767,10 @@
|
||||
|
||||
try
|
||||
{
|
||||
var result = await ApiDenuncias.ImportReportAsync(report, CancellationToken.None);
|
||||
var result = await ApiDenuncias.ImportReportAsync(
|
||||
report,
|
||||
report.RequiresOwnerConfirmation,
|
||||
CancellationToken.None);
|
||||
importedCount += result.ImportedCount;
|
||||
errors.AddRange(result.Errors.Select(error => $"#{report.Progressive ?? 0}: {error}"));
|
||||
if (result.Warnings is not null)
|
||||
@@ -685,6 +804,20 @@
|
||||
{
|
||||
SetStatus($"Se han importado {importedCount} denuncia(s) desde GlobalLeaks.", "alert-success");
|
||||
}
|
||||
else if (errors.Count == 0 && importedCount == 0)
|
||||
{
|
||||
var parts = new List<string>
|
||||
{
|
||||
"No se ha incorporado ninguna denuncia nueva."
|
||||
};
|
||||
|
||||
if (warnings.Count > 0)
|
||||
{
|
||||
parts.Add($"Avisos: {string.Join(" | ", warnings)}");
|
||||
}
|
||||
|
||||
SetStatus(string.Join(" ", parts), "alert-warning");
|
||||
}
|
||||
else
|
||||
{
|
||||
var parts = new List<string>
|
||||
@@ -713,6 +846,12 @@
|
||||
|
||||
private async Task OpenReportDetailAsync(ReportDto report)
|
||||
{
|
||||
if (report.Accessible == false)
|
||||
{
|
||||
SetStatus($"La denuncia #{report.Progressive ?? 0} no es accesible para este usuario en GlobalLeaks.", "alert-warning");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!CanUseGlobalLeaks)
|
||||
{
|
||||
SetStatus("Renueva antes la sesion de GlobalLeaks para consultar el detalle.", "alert-warning");
|
||||
@@ -790,7 +929,10 @@
|
||||
filtered = Filter switch
|
||||
{
|
||||
"new" => filtered.Where(report => string.IsNullOrWhiteSpace(report.AccessDate) || string.Equals(report.Status, "new", StringComparison.OrdinalIgnoreCase)),
|
||||
"updated" => filtered.Where(report => report.Updated),
|
||||
"updated" => filtered.Where(report =>
|
||||
report.CitizenHasNewActivity ||
|
||||
(!report.ActivityAnalyzed && report.Updated)),
|
||||
"receiver" => filtered.Where(report => report.ReceiverHasNewActivity),
|
||||
_ => filtered
|
||||
};
|
||||
|
||||
@@ -813,7 +955,10 @@
|
||||
.OrderByDescending(report => report.Progressive ?? 0)
|
||||
.ToList();
|
||||
|
||||
var validIds = Reports.Select(report => report.Id).ToHashSet(StringComparer.OrdinalIgnoreCase);
|
||||
var validIds = Reports
|
||||
.Where(CanUseReport)
|
||||
.Select(report => report.Id)
|
||||
.ToHashSet(StringComparer.OrdinalIgnoreCase);
|
||||
SelectedIds.RemoveWhere(id => !validIds.Contains(id));
|
||||
}
|
||||
|
||||
@@ -887,16 +1032,23 @@
|
||||
return reports;
|
||||
}
|
||||
|
||||
private void ToggleSelection(string reportId, ChangeEventArgs args)
|
||||
private void ToggleSelection(ReportDto report, ChangeEventArgs args)
|
||||
{
|
||||
if (report.Accessible == false)
|
||||
{
|
||||
SelectedIds.Remove(report.Id);
|
||||
StateHasChanged();
|
||||
return;
|
||||
}
|
||||
|
||||
var isChecked = GetCheckedValue(args);
|
||||
if (isChecked)
|
||||
{
|
||||
SelectedIds.Add(reportId);
|
||||
SelectedIds.Add(report.Id);
|
||||
}
|
||||
else
|
||||
{
|
||||
SelectedIds.Remove(reportId);
|
||||
SelectedIds.Remove(report.Id);
|
||||
}
|
||||
|
||||
StateHasChanged();
|
||||
@@ -904,9 +1056,12 @@
|
||||
|
||||
private void ToggleSelectAll()
|
||||
{
|
||||
var shouldSelect = !VisibleReports.All(report => SelectedIds.Contains(report.Id));
|
||||
var selectableReports = VisibleReports
|
||||
.Where(CanUseReport)
|
||||
.ToList();
|
||||
var shouldSelect = selectableReports.Count > 0 && !selectableReports.All(report => SelectedIds.Contains(report.Id));
|
||||
|
||||
foreach (var report in VisibleReports)
|
||||
foreach (var report in selectableReports)
|
||||
{
|
||||
if (shouldSelect)
|
||||
{
|
||||
@@ -980,15 +1135,70 @@
|
||||
}
|
||||
|
||||
private static string FormatDate(string? value)
|
||||
{
|
||||
return FormatOptionalDate(value) ?? "-";
|
||||
}
|
||||
|
||||
private static string? FormatOptionalDate(string? value)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(value))
|
||||
{
|
||||
return "-";
|
||||
return null;
|
||||
}
|
||||
|
||||
return DateTimeOffset.TryParse(value, out var parsed)
|
||||
? parsed.ToLocalTime().ToString("dd/MM/yyyy HH:mm")
|
||||
: "-";
|
||||
: null;
|
||||
}
|
||||
|
||||
private static string FormatCitizenActivity(ReportDto report)
|
||||
{
|
||||
var citizenDate = FormatOptionalDate(report.CitizenLastActivity);
|
||||
if (!string.IsNullOrWhiteSpace(citizenDate))
|
||||
{
|
||||
return citizenDate;
|
||||
}
|
||||
|
||||
if (!report.ActivityAnalyzed)
|
||||
{
|
||||
return "No analizada";
|
||||
}
|
||||
|
||||
var accessDate = FormatOptionalDate(report.WhistleblowerLastAccess);
|
||||
if (!string.IsNullOrWhiteSpace(accessDate))
|
||||
{
|
||||
return accessDate;
|
||||
}
|
||||
|
||||
return "Sin fecha";
|
||||
}
|
||||
|
||||
private static string FormatReceiverActivity(ReportDto report)
|
||||
{
|
||||
var receiverDate = FormatOptionalDate(report.ReceiverLastActivity);
|
||||
if (!string.IsNullOrWhiteSpace(receiverDate))
|
||||
{
|
||||
return string.IsNullOrWhiteSpace(report.ReceiverLastActivityAuthorName)
|
||||
? receiverDate
|
||||
: $"{receiverDate} · {report.ReceiverLastActivityAuthorName}";
|
||||
}
|
||||
|
||||
if (!report.ActivityAnalyzed)
|
||||
{
|
||||
return "No analizada";
|
||||
}
|
||||
|
||||
return "Sin actividad";
|
||||
}
|
||||
|
||||
private static string GetReceiverActivityTitle(ReportDto report)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(report.ReceiverLastActivityAuthorName))
|
||||
{
|
||||
return FormatReceiverActivity(report);
|
||||
}
|
||||
|
||||
return $"Ultima actividad OAAF realizada por {report.ReceiverLastActivityAuthorName}.";
|
||||
}
|
||||
|
||||
private static string FormatBytes(long? value)
|
||||
@@ -1012,16 +1222,45 @@
|
||||
return $"{bytes / 1024d / 1024d:0.#} MB";
|
||||
}
|
||||
|
||||
private static string GetCommentAuthorLabel(string? type)
|
||||
=> string.Equals(type, "whistleblower", StringComparison.OrdinalIgnoreCase)
|
||||
private static string GetCommentAuthorLabel(ReportCommentDto comment)
|
||||
=> IsWhistleblowerActivityType(comment.Type)
|
||||
? "Denunciante"
|
||||
: string.Equals(type, "receiver", StringComparison.OrdinalIgnoreCase)
|
||||
? "Receptor"
|
||||
: IsReceiverActivityType(comment.Type)
|
||||
? string.IsNullOrWhiteSpace(comment.AuthorName)
|
||||
? "Gestor OAAF"
|
||||
: $"Gestor OAAF: {comment.AuthorName}"
|
||||
: "Comentario";
|
||||
|
||||
private static bool IsWhistleblowerActivityType(string? value)
|
||||
=> MatchesAny(value, "whistleblower", "citizen", "source", "tipper", "submitter", "denunciante");
|
||||
|
||||
private static bool IsReceiverActivityType(string? value)
|
||||
=> MatchesAny(value, "receiver", "recipient", "admin", "administrator", "operator", "staff", "custodian", "moderator", "gestor", "oaaf");
|
||||
|
||||
private static bool MatchesAny(string? value, params string[] candidates)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(value))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var normalized = NormalizeActivityType(value);
|
||||
return candidates.Any(candidate =>
|
||||
{
|
||||
var candidateToken = NormalizeActivityType(candidate);
|
||||
return normalized == candidateToken ||
|
||||
normalized.StartsWith(candidateToken + " ", StringComparison.Ordinal) ||
|
||||
normalized.EndsWith(" " + candidateToken, StringComparison.Ordinal) ||
|
||||
normalized.Contains(" " + candidateToken + " ", StringComparison.Ordinal);
|
||||
});
|
||||
}
|
||||
|
||||
private static string NormalizeActivityType(string value)
|
||||
=> Regex.Replace(value.Trim().ToLowerInvariant(), @"[^a-z0-9]+", " ").Trim();
|
||||
|
||||
private static DateTimeOffset? GetEffectiveMoment(ReportDto report)
|
||||
{
|
||||
return ParseDate(report.UpdateDate) ?? ParseDate(report.CreationDate);
|
||||
return ParseDate(report.CitizenLastActivity) ?? ParseDate(report.ReceiverLastActivity) ?? ParseDate(report.UpdateDate) ?? ParseDate(report.CreationDate);
|
||||
}
|
||||
|
||||
private static DateTimeOffset? ParseDate(string? value)
|
||||
@@ -1043,14 +1282,29 @@
|
||||
return "Sin leer";
|
||||
}
|
||||
|
||||
if (report.Updated)
|
||||
if (report.CitizenHasNewActivity)
|
||||
{
|
||||
return "Actualización ciudadano";
|
||||
}
|
||||
|
||||
if (IsReceiverOnlyUpdate(report))
|
||||
{
|
||||
return "Actividad OAAF";
|
||||
}
|
||||
|
||||
if (report.AlreadyInGestiona && !report.ActivityAnalyzed)
|
||||
{
|
||||
return "Sin comprobar";
|
||||
}
|
||||
|
||||
if (IsUpToDateInGestiona(report))
|
||||
{
|
||||
return "Actualizada";
|
||||
}
|
||||
|
||||
return string.Equals(report.Status, "closed", StringComparison.OrdinalIgnoreCase)
|
||||
? "Cerrada"
|
||||
: "Abierta";
|
||||
: "Nueva denuncia";
|
||||
}
|
||||
|
||||
private static string GetStatusBadgeCss(ReportDto report)
|
||||
@@ -1060,16 +1314,125 @@
|
||||
return "bg-warning text-dark";
|
||||
}
|
||||
|
||||
if (report.Updated)
|
||||
if (report.CitizenHasNewActivity)
|
||||
{
|
||||
return "bg-info text-dark";
|
||||
}
|
||||
|
||||
if (IsReceiverOnlyUpdate(report))
|
||||
{
|
||||
return "bg-secondary";
|
||||
}
|
||||
|
||||
if (report.AlreadyInGestiona && !report.ActivityAnalyzed)
|
||||
{
|
||||
return "bg-warning text-dark";
|
||||
}
|
||||
|
||||
if (IsUpToDateInGestiona(report))
|
||||
{
|
||||
return "bg-light text-dark";
|
||||
}
|
||||
|
||||
return string.Equals(report.Status, "closed", StringComparison.OrdinalIgnoreCase)
|
||||
? "bg-secondary"
|
||||
: "bg-primary";
|
||||
}
|
||||
|
||||
private static bool IsUpToDateInGestiona(ReportDto report)
|
||||
{
|
||||
if (!report.AlreadyInGestiona)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var lastUpload = ParseDate(report.LastGestionaUploadAt);
|
||||
var latestActivity = new[]
|
||||
{
|
||||
ParseDate(report.CitizenLastActivity),
|
||||
ParseDate(report.ReceiverLastActivity)
|
||||
}
|
||||
.Where(value => value is not null)
|
||||
.Select(value => value!.Value)
|
||||
.DefaultIfEmpty()
|
||||
.Max();
|
||||
|
||||
if (lastUpload is not null && latestActivity != default)
|
||||
{
|
||||
return lastUpload.Value >= latestActivity;
|
||||
}
|
||||
|
||||
return report.ActivityAnalyzed &&
|
||||
!report.CitizenHasNewActivity &&
|
||||
!report.ReceiverHasNewActivity;
|
||||
}
|
||||
|
||||
private static string GetStatusHelp(ReportDto report)
|
||||
=> GetStatusLabel(report) switch
|
||||
{
|
||||
"Nueva denuncia" => "La denuncia todavía no tiene expediente creado desde la aplicación y está pendiente de actuación.",
|
||||
"Actualización ciudadano" => "El ciudadano ha añadido un comentario, un fichero o una modificación posterior a la última subida a Gestiona.",
|
||||
"Actividad OAAF" => "Un gestor de la OAAF ha realizado una comunicación o añadido un fichero posterior a la última subida a Gestiona.",
|
||||
"Actualizada" => "La última subida a Gestiona es igual o posterior a la actividad del ciudadano y de la OAAF detectada en el buzón.",
|
||||
"Sin comprobar" => "No se ha podido comparar en este momento la actividad del buzón con la última subida a Gestiona.",
|
||||
"Cerrada" => "La denuncia figura cerrada en GlobalLeaks.",
|
||||
"Sin leer" => "La denuncia todavía no se ha abierto con este usuario en GlobalLeaks.",
|
||||
_ => string.Empty
|
||||
};
|
||||
|
||||
private static string GetAccessLabel(ReportDto report)
|
||||
=> report.Accessible switch
|
||||
{
|
||||
true => "Accesible",
|
||||
false => "Sin acceso",
|
||||
_ => "Sin dato"
|
||||
};
|
||||
|
||||
private static string GetAccessBadgeCss(ReportDto report)
|
||||
=> report.Accessible switch
|
||||
{
|
||||
true => "bg-success",
|
||||
false => "bg-danger",
|
||||
_ => "bg-light text-dark"
|
||||
};
|
||||
|
||||
private static string GetAccessHelp(ReportDto report)
|
||||
=> report.Accessible switch
|
||||
{
|
||||
true => "Tu usuario gestor del buzón tiene acceso a esta denuncia.",
|
||||
false => "GlobalLeaks indica que tu usuario gestor del buzón no tiene acceso a esta denuncia.",
|
||||
_ => "GlobalLeaks no ha informado si tu usuario puede acceder a esta denuncia."
|
||||
};
|
||||
|
||||
private static bool CanUseReport(ReportDto report)
|
||||
=> report.Accessible != false;
|
||||
|
||||
private static bool IsReceiverOnlyUpdate(ReportDto report)
|
||||
=> report.AlreadyInGestiona &&
|
||||
report.ActivityAnalyzed &&
|
||||
!report.CitizenHasNewActivity &&
|
||||
report.ReceiverHasNewActivity;
|
||||
|
||||
private static string? GetReportActionBlockReason(ReportDto report)
|
||||
{
|
||||
if (report.Accessible == false)
|
||||
{
|
||||
return "GlobalLeaks indica que esta denuncia no es accesible para este usuario.";
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private static string GetReportDetailTitle(ReportDto report)
|
||||
{
|
||||
if (report.Accessible == false)
|
||||
{
|
||||
return "GlobalLeaks indica que esta denuncia no es accesible para este usuario.";
|
||||
}
|
||||
|
||||
return "Consulta mensajes y ficheros. GlobalLeaks puede marcar la denuncia como leida.";
|
||||
}
|
||||
|
||||
private static string GetTrackingLabel(ReportDto report)
|
||||
{
|
||||
if (report.AlreadyInGestiona)
|
||||
@@ -1120,8 +1483,47 @@
|
||||
return "bg-light text-dark";
|
||||
}
|
||||
|
||||
private static string GetTrackingHelp(ReportDto report)
|
||||
{
|
||||
if (report.AlreadyInGestiona)
|
||||
{
|
||||
return "La denuncia ya tiene un expediente creado en Gestiona. El estado indica si existe actividad posterior pendiente.";
|
||||
}
|
||||
|
||||
if (report.DownloadedByAnotherUser)
|
||||
{
|
||||
return string.IsNullOrWhiteSpace(report.TrackingNote)
|
||||
? "Otro usuario ya la descargó en la aplicación, pero todavía no se ha materializado la subida a Gestiona."
|
||||
: $"{report.TrackingNote}. Todavía no se ha materializado la subida a Gestiona.";
|
||||
}
|
||||
|
||||
if (report.DownloadedByCurrentUser)
|
||||
{
|
||||
return string.IsNullOrWhiteSpace(report.TrackingNote)
|
||||
? "Ya la descargaste en la aplicación, pero todavía no se ha materializado la subida a Gestiona."
|
||||
: $"{report.TrackingNote}. Todavía no se ha materializado la subida a Gestiona.";
|
||||
}
|
||||
|
||||
if (report.AlreadyImported)
|
||||
{
|
||||
return "La denuncia está incorporada a la aplicación, pero todavía no se ha subido a Gestiona.";
|
||||
}
|
||||
|
||||
return "La denuncia todavía no se ha descargado ni subido a Gestiona.";
|
||||
}
|
||||
|
||||
private static string? GetReportRowCss(ReportDto report)
|
||||
{
|
||||
if (report.Accessible == false)
|
||||
{
|
||||
return "table-danger report-row-disabled";
|
||||
}
|
||||
|
||||
if (IsReceiverOnlyUpdate(report))
|
||||
{
|
||||
return "table-secondary";
|
||||
}
|
||||
|
||||
if (report.AlreadyInGestiona)
|
||||
{
|
||||
return "table-success";
|
||||
@@ -3,14 +3,16 @@
|
||||
@attribute [Authorize]
|
||||
@using GestionaDenunciasAN.Models
|
||||
@using GestionaDenunciasAN.Services
|
||||
@using GestionaDenuncias.Shared.Models
|
||||
@using System.Globalization
|
||||
@attribute [StreamRendering]
|
||||
@inject GestionaDenunciasAN.Models.UserState userState
|
||||
@inject NavigationManager Navigation
|
||||
@inject IHostEnvironment HostEnvironment
|
||||
@inject IDenunciaStore DenunciaStore
|
||||
@inject ApiDenunciasClient ApiDenuncias
|
||||
|
||||
<PageTitle>Denuncias Gesti<EFBFBD>n</PageTitle>
|
||||
<PageTitle>Denuncias Gestión</PageTitle>
|
||||
|
||||
<style>
|
||||
/* Contenedor para la lista de denuncias */
|
||||
@@ -64,7 +66,7 @@
|
||||
.card-body {
|
||||
padding: 1.25rem;
|
||||
}
|
||||
/* Estilos para los t<EFBFBD>tulos de secci<EFBFBD>n dentro de la card */
|
||||
/* Estilos para los títulos de sección dentro de la card */
|
||||
.section-heading {
|
||||
text-align: center;
|
||||
font-weight: bold;
|
||||
@@ -75,9 +77,9 @@
|
||||
}
|
||||
</style>
|
||||
|
||||
<h1>Denuncias en Gesti<EFBFBD>n</h1>
|
||||
<h1>Denuncias en Gestión</h1>
|
||||
|
||||
<!-- Campo de b<EFBFBD>squeda -->
|
||||
<!-- Campo de búsqueda -->
|
||||
<input type="text"
|
||||
class="form-control"
|
||||
placeholder="Buscar denuncias..."
|
||||
@@ -89,9 +91,10 @@
|
||||
{
|
||||
<div class="alert alert-info">Cargando datos...</div>
|
||||
}
|
||||
else if (denunciasGestiona == null || !denunciasGestiona.Any())
|
||||
else if ((historialGestiona == null || !historialGestiona.Any()) &&
|
||||
(denunciasGestiona == null || !denunciasGestiona.Any()))
|
||||
{
|
||||
<p>No hay denuncias en gesti<EFBFBD>n.</p>
|
||||
<p>No hay denuncias en gestión.</p>
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -105,18 +108,47 @@ else
|
||||
))
|
||||
{
|
||||
var collapseId = $"collapse{denuncia.Id_Denuncia}";
|
||||
var latestHistory = GetLatestHistory(denuncia.Id_Denuncia);
|
||||
var uploadMoment = GetUploadMoment(denuncia, latestHistory);
|
||||
<div class="card collapse-card Aceptada">
|
||||
<div class="card-header" data-bs-toggle="collapse" data-bs-target="#@collapseId" aria-expanded="false" aria-controls="@collapseId">
|
||||
<h5 class="mb-0">Denuncia ID: @denuncia.Id_Denuncia</h5>
|
||||
<div class="header-info">
|
||||
<span><strong>Estado:</strong> @denuncia.Estado</span>
|
||||
<span><strong>Asunto:</strong> @denuncia.NombreDenuncia</span>
|
||||
<span><strong>Fecha de Subida:</strong> @denuncia.FechaSubidaAGestiona.ToString("dd/MM/yyyy")</span>
|
||||
<span><strong>Hora de Subida:</strong> @denuncia.FechaSubidaAGestiona.ToString("HH:mm")</span>
|
||||
<span title="Asunto con el que se identifica el expediente en Gestiona."><strong>Asunto:</strong> @GetHeaderSubject(denuncia, latestHistory)</span>
|
||||
@if (!string.IsNullOrWhiteSpace(denuncia.ExpedienteGestionaMostrable))
|
||||
{
|
||||
<span title="Número asignado al expediente por Gestiona."><strong>Nº expediente:</strong> @denuncia.ExpedienteGestionaMostrable</span>
|
||||
}
|
||||
@if (!string.IsNullOrWhiteSpace(GetUploadType(denuncia, latestHistory)))
|
||||
{
|
||||
<span title="Tipo de la última operación enviada desde la aplicación a Gestiona."><strong>Última subida:</strong> @GetUploadType(denuncia, latestHistory)</span>
|
||||
}
|
||||
@if (!string.IsNullOrWhiteSpace(GetAssignedGroup(denuncia, latestHistory)))
|
||||
{
|
||||
<span title="Grupo al que quedó asignado el expediente tras la última subida."><strong>Asignación en Gestiona:</strong> @FormatGroupCode(GetAssignedGroup(denuncia, latestHistory))</span>
|
||||
}
|
||||
@if (!string.IsNullOrWhiteSpace(GetUploadUser(latestHistory)))
|
||||
{
|
||||
<span title="Usuario de la aplicación que realizó la última subida."><strong>Usuario subida:</strong> @GetUploadUser(latestHistory)</span>
|
||||
}
|
||||
<span title="Fecha de la última operación enviada a Gestiona."><strong>Fecha de Subida:</strong> @FormatUploadDate(uploadMoment)</span>
|
||||
<span title="Hora local de la última operación enviada a Gestiona."><strong>Hora de Subida:</strong> @FormatUploadTime(uploadMoment)</span>
|
||||
@if (!string.IsNullOrWhiteSpace(GetAuditDateText(denuncia)))
|
||||
{
|
||||
<span title="Último movimiento registrado en la auditoría del expediente de Gestiona."><strong>Última actividad Gestiona:</strong> @GetAuditDateText(denuncia)</span>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
<div id="@collapseId" class="collapse">
|
||||
<div class="card-body">
|
||||
@if (ficherosAdjuntosPurgados.Contains(denuncia.Id_Denuncia))
|
||||
{
|
||||
<div class="alert alert-warning mt-3">
|
||||
La informacion de detalle de esta denuncia no se puede mostrar porque sus datos estan purgados criptograficamente. Se mantiene visible la cabecera operativa del expediente.
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<!-- Datos Generales -->
|
||||
<h5 class="section-heading">Datos Generales</h5>
|
||||
<dl class="row">
|
||||
@@ -137,12 +169,22 @@ else
|
||||
}
|
||||
@if (!string.IsNullOrWhiteSpace(denuncia.ExpedienteGestionaMostrable))
|
||||
{
|
||||
<dt class="col-sm-3">N<EFBFBD> expediente Gestiona</dt>
|
||||
<dt class="col-sm-3">Nº expediente Gestiona</dt>
|
||||
<dd class="col-sm-9">@denuncia.ExpedienteGestionaMostrable</dd>
|
||||
}
|
||||
@if (!string.IsNullOrWhiteSpace(GetAuditDateText(denuncia)))
|
||||
{
|
||||
<dt class="col-sm-3">Última actividad Gestiona</dt>
|
||||
<dd class="col-sm-9">@GetAuditDateText(denuncia)</dd>
|
||||
}
|
||||
@if (!string.IsNullOrWhiteSpace(GetAuditMessage(denuncia)))
|
||||
{
|
||||
<dt class="col-sm-3">Última acción Gestiona</dt>
|
||||
<dd class="col-sm-9">@GetAuditMessage(denuncia)</dd>
|
||||
}
|
||||
@if (denuncia.Id_Persona_Gestiona != 0)
|
||||
{
|
||||
<dt class="col-sm-3">ID Persona Gesti<EFBFBD>n</dt>
|
||||
<dt class="col-sm-3">ID Persona Gestión</dt>
|
||||
<dd class="col-sm-9">@denuncia.Id_Persona_Gestiona</dd>
|
||||
}
|
||||
@if (!string.IsNullOrWhiteSpace(denuncia.Etiqueta))
|
||||
@@ -191,13 +233,13 @@ else
|
||||
<dd class="col-sm-9">@denuncia.Asunto</dd>
|
||||
<dt class="col-sm-3">A Quien Denuncia</dt>
|
||||
<dd class="col-sm-9">@denuncia.A_Quien_Denuncia</dd>
|
||||
<dt class="col-sm-3">Descripci<EFBFBD>n Denuncia</dt>
|
||||
<dt class="col-sm-3">Descripción Denuncia</dt>
|
||||
<dd class="col-sm-9">@denuncia.Descripcion_Denuncia</dd>
|
||||
<dt class="col-sm-3">Denunciado Ante Inst</dt>
|
||||
<dd class="col-sm-9">@denuncia.Denunciado_Ante_Inst</dd>
|
||||
@if (!string.IsNullOrWhiteSpace(denuncia.Modalidad_Informacion))
|
||||
{
|
||||
<dt class="col-sm-3">Modalidad Informaci<EFBFBD>n</dt>
|
||||
<dt class="col-sm-3">Modalidad Información</dt>
|
||||
<dd class="col-sm-9">@denuncia.Modalidad_Informacion</dd>
|
||||
}
|
||||
<dt class="col-sm-3">Lugar Hechos</dt>
|
||||
@@ -209,27 +251,27 @@ else
|
||||
}
|
||||
</dl>
|
||||
|
||||
<!-- Datos de Notificaci<EFBFBD>n -->
|
||||
<h5 class="section-heading">Datos de Notificaci<EFBFBD>n</h5>
|
||||
<!-- Datos de Notificación -->
|
||||
<h5 class="section-heading">Datos de Notificación</h5>
|
||||
<dl class="row">
|
||||
@if (!string.IsNullOrWhiteSpace(denuncia.Notificacion_Preferencia))
|
||||
{
|
||||
<dt class="col-sm-3">Notificaci<EFBFBD>n Preferencia</dt>
|
||||
<dt class="col-sm-3">Notificación Preferencia</dt>
|
||||
<dd class="col-sm-9">@denuncia.Notificacion_Preferencia</dd>
|
||||
}
|
||||
@if (!string.IsNullOrWhiteSpace(denuncia.Notificacion_Electronica))
|
||||
{
|
||||
<dt class="col-sm-3">Notificaci<EFBFBD>n Electr<EFBFBD>nica</dt>
|
||||
<dt class="col-sm-3">Notificación Electrónica</dt>
|
||||
<dd class="col-sm-9">@denuncia.Notificacion_Electronica</dd>
|
||||
}
|
||||
@if (!string.IsNullOrWhiteSpace(denuncia.Correo_Electronico))
|
||||
{
|
||||
<dt class="col-sm-3">Correo Electr<EFBFBD>nico</dt>
|
||||
<dt class="col-sm-3">Correo Electrónico</dt>
|
||||
<dd class="col-sm-9">@denuncia.Correo_Electronico</dd>
|
||||
}
|
||||
@if (!string.IsNullOrWhiteSpace(denuncia.Notificacion_Sms))
|
||||
{
|
||||
<dt class="col-sm-3">Notificaci<EFBFBD>n SMS</dt>
|
||||
<dt class="col-sm-3">Notificación SMS</dt>
|
||||
<dd class="col-sm-9">@denuncia.Notificacion_Sms</dd>
|
||||
}
|
||||
</dl>
|
||||
@@ -240,7 +282,7 @@ else
|
||||
@if (denuncia.Condiciones)
|
||||
{
|
||||
<dt class="col-sm-3">Condiciones</dt>
|
||||
<dd class="col-sm-9">S<EFBFBD></dd>
|
||||
<dd class="col-sm-9">Sí</dd>
|
||||
}
|
||||
@if (!string.IsNullOrWhiteSpace(denuncia.Comments))
|
||||
{
|
||||
@@ -257,7 +299,7 @@ else
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Nombre</th>
|
||||
<th>Tama<EFBFBD>o (bytes)</th>
|
||||
<th>Tamaño (bytes)</th>
|
||||
<th>Ver</th>
|
||||
</tr>
|
||||
</thead>
|
||||
@@ -277,6 +319,29 @@ else
|
||||
</tbody>
|
||||
</table>
|
||||
}
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
@foreach (var item in GetHeaderOnlyHistoryItems())
|
||||
{
|
||||
var uploadMoment = item.UploadedAtUtc;
|
||||
<div class="card collapse-card Aceptada">
|
||||
<div class="card-header">
|
||||
<h5 class="mb-0">Denuncia ID: @item.DenunciaId</h5>
|
||||
<div class="header-info">
|
||||
<span title="Asunto con el que se identifica el expediente en Gestiona."><strong>Asunto:</strong> @DisplayOrDash(item.Asunto)</span>
|
||||
<span title="Número asignado al expediente por Gestiona."><strong>Nº expediente:</strong> @DisplayOrDash(item.CodigoExpedienteGestiona)</span>
|
||||
<span title="Tipo de la última operación enviada desde la aplicación a Gestiona."><strong>Última subida:</strong> @DisplayOrDash(item.TipoSubida)</span>
|
||||
<span title="Grupo al que quedó asignado el expediente tras la última subida."><strong>Asignación en Gestiona:</strong> @DisplayOrDash(FormatGroupCode(item.GrupoAsignado))</span>
|
||||
@if (!string.IsNullOrWhiteSpace(GetUploadUser(item)))
|
||||
{
|
||||
<span title="Usuario de la aplicación que realizó la última subida."><strong>Usuario subida:</strong> @GetUploadUser(item)</span>
|
||||
}
|
||||
<span title="Fecha de la última operación enviada a Gestiona."><strong>Fecha de Subida:</strong> @FormatUploadDate(uploadMoment)</span>
|
||||
<span title="Hora local de la última operación enviada a Gestiona."><strong>Hora de Subida:</strong> @FormatUploadTime(uploadMoment)</span>
|
||||
<span class="text-muted">Detalle no disponible por purga criptográfica.</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -305,9 +370,12 @@ else
|
||||
|
||||
@code {
|
||||
private List<DenunciasGestiona> denunciasGestiona = new();
|
||||
private List<GestionaUploadHistoryEntry> historialGestiona = new();
|
||||
private Dictionary<int, List<FicherosDenuncias>> ficherosAdjuntos = new();
|
||||
private HashSet<int> ficherosAdjuntosPurgados = new();
|
||||
private Dictionary<int, GestionaAuditInfo> auditoriasGestiona = new();
|
||||
|
||||
// Variable para la b<EFBFBD>squeda
|
||||
// Variable para la búsqueda
|
||||
private string busqueda = "";
|
||||
|
||||
private bool hasLoaded = false;
|
||||
@@ -316,8 +384,10 @@ else
|
||||
{
|
||||
if (firstRender)
|
||||
{
|
||||
await CargarHistorialGestionaAsync();
|
||||
await CargarGestionaAsync();
|
||||
await CargarFicherosAdjuntosAsync(denunciasGestiona.Select(d => d.Id_Denuncia));
|
||||
await CargarAuditoriasGestionaAsync(denunciasGestiona);
|
||||
hasLoaded = true;
|
||||
StateHasChanged();
|
||||
}
|
||||
@@ -331,16 +401,206 @@ else
|
||||
.ToList();
|
||||
}
|
||||
|
||||
|
||||
private async Task CargarHistorialGestionaAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
historialGestiona = await ApiDenuncias.GetGestionaUploadHistoryAsync();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.Error.WriteLine($"No se ha podido cargar el histórico de subidas a Gestiona: {ex}");
|
||||
historialGestiona = new List<GestionaUploadHistoryEntry>();
|
||||
}
|
||||
}
|
||||
private async Task<List<DenunciasGestiona>> CargarDenunciasJsonAsync()
|
||||
{
|
||||
return await DenunciaStore.GetDenunciasByScopeAsync(DenunciaListScope.InGestiona);
|
||||
}
|
||||
|
||||
private async Task CargarAuditoriasGestionaAsync(IEnumerable<DenunciasGestiona> denuncias)
|
||||
{
|
||||
auditoriasGestiona.Clear();
|
||||
var items = denuncias
|
||||
.Where(d => d.Id_Denuncia > 0 && !string.IsNullOrWhiteSpace(d.Expediente_Gestiona) && !string.Equals(d.Expediente_Gestiona, "Pendiente", StringComparison.OrdinalIgnoreCase))
|
||||
.DistinctBy(d => d.Id_Denuncia)
|
||||
.ToList();
|
||||
|
||||
using var gate = new System.Threading.SemaphoreSlim(4, 4);
|
||||
var tasks = items.Select(async denuncia =>
|
||||
{
|
||||
await gate.WaitAsync();
|
||||
try
|
||||
{
|
||||
var audit = await ApiDenuncias.GetGestionaFileLatestAuditAsync(denuncia.Expediente_Gestiona);
|
||||
if (audit?.Fecha is null && string.IsNullOrWhiteSpace(audit?.Mensaje))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
lock (auditoriasGestiona)
|
||||
{
|
||||
auditoriasGestiona[denuncia.Id_Denuncia] = audit;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.Error.WriteLine($"No se ha podido cargar la auditoría de Gestiona para la denuncia {denuncia.Id_Denuncia}: {ex}");
|
||||
}
|
||||
finally
|
||||
{
|
||||
gate.Release();
|
||||
}
|
||||
});
|
||||
|
||||
await Task.WhenAll(tasks);
|
||||
}
|
||||
|
||||
private string GetAuditDateText(DenunciasGestiona denuncia)
|
||||
{
|
||||
return auditoriasGestiona.TryGetValue(denuncia.Id_Denuncia, out var audit) && audit.Fecha is not null
|
||||
? audit.Fecha.Value.ToLocalTime().ToString("dd/MM/yyyy HH:mm")
|
||||
: string.Empty;
|
||||
}
|
||||
|
||||
private string GetAuditMessage(DenunciasGestiona denuncia)
|
||||
{
|
||||
return auditoriasGestiona.TryGetValue(denuncia.Id_Denuncia, out var audit)
|
||||
? audit.Mensaje ?? string.Empty
|
||||
: string.Empty;
|
||||
}
|
||||
|
||||
private GestionaUploadHistoryEntry? GetLatestHistory(int denunciaId)
|
||||
{
|
||||
return historialGestiona
|
||||
.Where(item => item.DenunciaId == denunciaId)
|
||||
.OrderByDescending(item => item.UploadedAtUtc)
|
||||
.ThenByDescending(item => item.Id)
|
||||
.FirstOrDefault();
|
||||
}
|
||||
|
||||
private IEnumerable<GestionaUploadHistoryEntry> GetHeaderOnlyHistoryItems()
|
||||
{
|
||||
var visibleComplaintIds = denunciasGestiona
|
||||
.Select(denuncia => denuncia.Id_Denuncia)
|
||||
.ToHashSet();
|
||||
|
||||
return historialGestiona
|
||||
.Where(item => !visibleComplaintIds.Contains(item.DenunciaId))
|
||||
.Where(MatchesHistorySearch)
|
||||
.GroupBy(item => item.DenunciaId)
|
||||
.Select(group => group
|
||||
.OrderByDescending(item => item.UploadedAtUtc)
|
||||
.ThenByDescending(item => item.Id)
|
||||
.First())
|
||||
.OrderByDescending(item => item.UploadedAtUtc)
|
||||
.ThenByDescending(item => item.Id);
|
||||
}
|
||||
|
||||
private static DateTime GetUploadMoment(DenunciasGestiona denuncia, GestionaUploadHistoryEntry? history)
|
||||
{
|
||||
if (history?.UploadedAtUtc is DateTime historyDate && historyDate != DateTime.MinValue)
|
||||
{
|
||||
return historyDate;
|
||||
}
|
||||
|
||||
return denuncia.FechaSubidaAGestiona;
|
||||
}
|
||||
|
||||
private static string GetHeaderSubject(DenunciasGestiona denuncia, GestionaUploadHistoryEntry? history)
|
||||
{
|
||||
return !string.IsNullOrWhiteSpace(history?.Asunto)
|
||||
? history.Asunto.Trim()
|
||||
: DisplayOrDash(denuncia.NombreDenuncia);
|
||||
}
|
||||
|
||||
private static string GetUploadType(DenunciasGestiona denuncia, GestionaUploadHistoryEntry? history)
|
||||
{
|
||||
return !string.IsNullOrWhiteSpace(history?.TipoSubida)
|
||||
? history.TipoSubida.Trim()
|
||||
: denuncia.UltimaSubidaGestionaTipoMostrable;
|
||||
}
|
||||
|
||||
private static string GetAssignedGroup(DenunciasGestiona denuncia, GestionaUploadHistoryEntry? history)
|
||||
{
|
||||
return !string.IsNullOrWhiteSpace(history?.GrupoAsignado)
|
||||
? history.GrupoAsignado.Trim()
|
||||
: denuncia.UltimoGrupoAsignadoGestionaMostrable;
|
||||
}
|
||||
|
||||
private static string GetUploadUser(GestionaUploadHistoryEntry? history)
|
||||
{
|
||||
return string.IsNullOrWhiteSpace(history?.Usuario) ||
|
||||
string.Equals(history.Usuario, "No registrado", StringComparison.OrdinalIgnoreCase)
|
||||
? string.Empty
|
||||
: history.Usuario.Trim();
|
||||
}
|
||||
|
||||
private static string FormatUploadDate(DateTime value)
|
||||
{
|
||||
var local = ToLocalUploadTime(value);
|
||||
return local == DateTime.MinValue ? "-" : local.ToString("dd/MM/yyyy");
|
||||
}
|
||||
|
||||
private static string FormatUploadTime(DateTime value)
|
||||
{
|
||||
var local = ToLocalUploadTime(value);
|
||||
return local == DateTime.MinValue ? "-" : local.ToString("HH:mm");
|
||||
}
|
||||
|
||||
private static DateTime ToLocalUploadTime(DateTime value)
|
||||
{
|
||||
return value == DateTime.MinValue
|
||||
? DateTime.MinValue
|
||||
: DateTime.SpecifyKind(value, DateTimeKind.Utc).ToLocalTime();
|
||||
}
|
||||
|
||||
private bool MatchesHistorySearch(GestionaUploadHistoryEntry item)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(busqueda))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
var search = busqueda.Trim();
|
||||
return item.DenunciaId.ToString(CultureInfo.InvariantCulture).Contains(search, StringComparison.OrdinalIgnoreCase) ||
|
||||
(item.CodigoExpedienteGestiona ?? string.Empty).Contains(search, StringComparison.OrdinalIgnoreCase) ||
|
||||
(item.TipoSubida ?? string.Empty).Contains(search, StringComparison.OrdinalIgnoreCase) ||
|
||||
(item.GrupoAsignado ?? string.Empty).Contains(search, StringComparison.OrdinalIgnoreCase) ||
|
||||
(item.Usuario ?? string.Empty).Contains(search, StringComparison.OrdinalIgnoreCase) ||
|
||||
(item.Asunto ?? string.Empty).Contains(search, StringComparison.OrdinalIgnoreCase) ||
|
||||
(item.Documentos ?? string.Empty).Contains(search, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
private static string FormatHistoryDate(DateTime value)
|
||||
{
|
||||
var local = ToLocalUploadTime(value);
|
||||
return local == DateTime.MinValue
|
||||
? "-"
|
||||
: local.ToString("dd/MM/yyyy HH:mm");
|
||||
}
|
||||
|
||||
private static string DisplayOrDash(string? value)
|
||||
=> string.IsNullOrWhiteSpace(value) ? "-" : value.Trim();
|
||||
|
||||
private static string FormatGroupCode(string? value)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(value))
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
var trimmed = value.Trim();
|
||||
var code = new string(trimmed.TakeWhile(char.IsDigit).ToArray());
|
||||
return string.IsNullOrWhiteSpace(code) ? trimmed : code;
|
||||
}
|
||||
private async Task CargarFicherosAdjuntosAsync(IEnumerable<int> denunciaIds)
|
||||
{
|
||||
ficherosAdjuntos.Clear();
|
||||
ficherosAdjuntosPurgados.Clear();
|
||||
foreach (var denunciaId in denunciaIds.Where(id => id > 0).Distinct())
|
||||
{
|
||||
try
|
||||
{
|
||||
var ficheros = await DenunciaStore.GetFicherosByDenunciaAsync(denunciaId);
|
||||
if (ficheros.Count > 0)
|
||||
@@ -348,6 +608,19 @@ else
|
||||
ficherosAdjuntos[denunciaId] = ficheros;
|
||||
}
|
||||
}
|
||||
catch (InvalidOperationException ex) when (IsPurgedDataError(ex))
|
||||
{
|
||||
ficherosAdjuntosPurgados.Add(denunciaId);
|
||||
Console.Error.WriteLine($"No se han podido cargar los adjuntos de la denuncia {denunciaId} porque estan purgados: {ex.Message}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static bool IsPurgedDataError(Exception ex)
|
||||
{
|
||||
var message = ex.Message ?? string.Empty;
|
||||
return message.Contains("datos purgados", StringComparison.OrdinalIgnoreCase) ||
|
||||
message.Contains("Denuncia no disponible", StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
private static string BuildAttachmentContentUrl(int denunciaId, string? fileName)
|
||||
|
||||
@@ -56,7 +56,7 @@
|
||||
<li>Abre la denuncia para revisar sus datos y adjuntos.</li>
|
||||
<li>En la lista de ficheros, deja marcado solo lo que quieras subir.</li>
|
||||
<li>El report de la denuncia se sube siempre y no se puede desmarcar.</li>
|
||||
<li>Pulsa <strong>Configurar subida</strong> para indicar asunto, grupo destino y modo de subida.</li>
|
||||
<li>Pulsa <strong>Configurar apertura expediente</strong> para indicar asunto, grupo destino y modo de subida.</li>
|
||||
<li>Confirma para crear el expediente, vincular el tercero y subir los documentos a Gestiona.</li>
|
||||
<li>Si la denuncia no procede, usa <strong>Rechazar denuncia</strong> e indica el motivo.</li>
|
||||
</ul>
|
||||
@@ -67,15 +67,15 @@
|
||||
<div class="col-12 col-xl-6">
|
||||
<div class="card h-100">
|
||||
<div class="card-body">
|
||||
<h2 class="h5">Modal de subida a Gestiona</h2>
|
||||
<h2 class="h5">Ventana de configuración de la subida</h2>
|
||||
<p>
|
||||
Antes de confirmar la subida, revisa estos puntos:
|
||||
Al configurar un expediente nuevo o una actualización, revisa estos puntos antes de confirmar:
|
||||
</p>
|
||||
<ul class="mb-0">
|
||||
<li><strong>Asunto</strong>: texto que identificara el expediente/documentos en Gestiona.</li>
|
||||
<li><strong>Grupo destino</strong>: unidad a la que se asignara el expediente.</li>
|
||||
<li><strong>Asunto</strong>: texto que identifica el expediente en Gestiona. En las actualizaciones se muestra en modo de solo lectura.</li>
|
||||
<li><strong>Grupo destino</strong>: grupo al que quedará asignado el expediente en Gestiona.</li>
|
||||
<li><strong>Modo de subida</strong>: puedes unir adjuntos en un PDF o subirlos de forma independiente.</li>
|
||||
<li><strong>Tercero</strong>: la app lo rellena desde la denuncia. Si es anonima, se usa el tercero anonimo configurado.</li>
|
||||
<li><strong>Tercero</strong>: la aplicación lo completa con los datos de la denuncia. Si es anónima, se utiliza el tercero anónimo configurado.</li>
|
||||
<li><strong>Expedientes del tercero</strong>: puedes consultarlos antes de confirmar si necesitas contexto.</li>
|
||||
</ul>
|
||||
</div>
|
||||
@@ -87,14 +87,15 @@
|
||||
<div class="card-body">
|
||||
<h2 class="h5">Actualizaciones</h2>
|
||||
<p>
|
||||
Esta pantalla recoge comunicaciones o adjuntos nuevos sobre denuncias que ya tienen expediente en Gestiona.
|
||||
Esta pantalla recoge comentarios, comunicaciones o adjuntos nuevos sobre denuncias que ya tienen expediente en Gestiona.
|
||||
</p>
|
||||
<ul class="mb-0">
|
||||
<li>Revisa la actualizacion y sus ficheros.</li>
|
||||
<li>La app propone los adjuntos que parecen nuevos.</li>
|
||||
<li>Puedes desmarcar los adjuntos que no quieras subir.</li>
|
||||
<li>El report de la actualizacion se mantiene obligatorio.</li>
|
||||
<li>Confirma la subida para a<EFBFBD>adir los nuevos documentos al expediente existente.</li>
|
||||
<li>Pulsa <strong>Configurar actualización expediente</strong> y confirma para añadir el cambio al expediente existente.</li>
|
||||
<li>Si la actividad procede de la OAAF, la aplicación utiliza el aviso correspondiente al grupo SAJ o SDI.</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
@@ -105,12 +106,12 @@
|
||||
<div class="card-body">
|
||||
<h2 class="h5">Gestiona</h2>
|
||||
<p>
|
||||
Aqui se consultan las denuncias que ya se han enviado a Gestiona.
|
||||
Aquí se almacena el histórico permanente de los movimientos enviados desde esta aplicación a Gestiona.
|
||||
</p>
|
||||
<ul class="mb-0">
|
||||
<li>Comprueba el numero de expediente y la fecha de envio.</li>
|
||||
<li>Accede al enlace del expediente cuando necesites revisar la tramitacion en Gestiona.</li>
|
||||
<li>Usa esta pantalla como seguimiento de lo que ya salio de Pendientes o Actualizaciones.</li>
|
||||
<li>Comprueba el número de expediente, el tipo de subida, el usuario que la realizó y la asignación en Gestiona.</li>
|
||||
<li>Despliega una operación del día para consultar sus datos y documentos mientras estén disponibles.</li>
|
||||
<li>Por requisitos de seguridad ENS, los detalles sensibles de días anteriores dejan de estar disponibles; la cabecera operativa y la trazabilidad permanecen visibles.</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
@@ -121,7 +122,7 @@
|
||||
<div class="card-body">
|
||||
<h2 class="h5">Rechazados</h2>
|
||||
<p>
|
||||
Aqui quedan las denuncias que se han descartado desde Pendientes.
|
||||
Aquí quedan las denuncias o actualizaciones que no se han subido a Gestiona porque se descartaron desde la pantalla de trabajo.
|
||||
</p>
|
||||
<ul class="mb-0">
|
||||
<li>Consulta el motivo indicado al rechazar.</li>
|
||||
|
||||
@@ -355,7 +355,7 @@
|
||||
return ReturnUrl;
|
||||
}
|
||||
|
||||
return "/GestionZip";
|
||||
return "/Entrada";
|
||||
}
|
||||
|
||||
private static T? ReadData<T>(ApiJsResponse response)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
@page "/Pendientes"
|
||||
@page "/Pendientes"
|
||||
@rendermode InteractiveServer
|
||||
@attribute [Authorize]
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
@inject IDenunciaStore DenunciaStore
|
||||
@inject ApiDenunciasClient ApiDenuncias
|
||||
@inject UiBusyService Busy
|
||||
@inject UiDialogService Dialogs
|
||||
|
||||
<PageTitle>Denuncias Pendientes</PageTitle>
|
||||
|
||||
@@ -199,22 +200,27 @@ else
|
||||
data-bs-target="#@collapseId"
|
||||
aria-expanded="false"
|
||||
aria-controls="@collapseId">
|
||||
<div class="d-flex flex-wrap align-items-center gap-2">
|
||||
<h5 class="mb-0">Denuncia ID: @denuncia.Id_Denuncia</h5>
|
||||
@if (!string.IsNullOrWhiteSpace(denuncia.OwnerUsername))
|
||||
{
|
||||
<span class="badge @(denuncia.OwnedByCurrentUser ? "text-bg-secondary" : "text-bg-warning")">
|
||||
Responsable: @denuncia.OwnerUsername
|
||||
</span>
|
||||
}
|
||||
@if (denuncia.OwnerWorkGroups.Count > 0)
|
||||
{
|
||||
<span class="badge text-bg-light">
|
||||
Grupo: @string.Join(" / ", denuncia.OwnerWorkGroups)
|
||||
</span>
|
||||
}
|
||||
</div>
|
||||
<div>
|
||||
<button type="button"
|
||||
class="btn btn-success btn-sm me-2"
|
||||
@onclick:stopPropagation="true"
|
||||
@onclick="() => OpenEnviarAGestionaModal(denuncia)">
|
||||
Configurar subida
|
||||
</button>
|
||||
|
||||
<!-- Enviar a Actualizaciones -->
|
||||
<button type="button"
|
||||
class="btn btn-outline-warning btn-sm me-2"
|
||||
title="Mover esta denuncia a la cola de Actualizaciones"
|
||||
@onclick:stopPropagation="true"
|
||||
@onclick="() => MoverAActualizaciones(denuncia)">
|
||||
Enviar a Actualizaciones
|
||||
Configurar apertura expediente
|
||||
</button>
|
||||
|
||||
<button type="button"
|
||||
@@ -247,7 +253,7 @@ else
|
||||
}
|
||||
@if (!string.IsNullOrWhiteSpace(denuncia.ExpedienteGestionaMostrable))
|
||||
{
|
||||
<dt class="col-sm-3">N<EFBFBD> expediente Gestiona</dt>
|
||||
<dt class="col-sm-3">Nº expediente Gestiona</dt>
|
||||
<dd class="col-sm-9">@denuncia.ExpedienteGestionaMostrable</dd>
|
||||
}
|
||||
@if (denuncia.Id_Persona_Gestiona != 0)
|
||||
@@ -287,7 +293,7 @@ else
|
||||
}
|
||||
@if (!string.IsNullOrWhiteSpace(denuncia.RazonSocialResuelta))
|
||||
{
|
||||
<dt class="col-sm-3">Raz<EFBFBD>n social</dt>
|
||||
<dt class="col-sm-3">Razón social</dt>
|
||||
<dd class="col-sm-9">@denuncia.RazonSocialResuelta</dd>
|
||||
}
|
||||
@if (!string.IsNullOrWhiteSpace(denuncia.NombreResuelto))
|
||||
@@ -297,12 +303,12 @@ else
|
||||
}
|
||||
@if (!string.IsNullOrWhiteSpace(denuncia.PrimerApellidoResuelto))
|
||||
{
|
||||
<dt class="col-sm-3">1<EFBFBD> Apellido</dt>
|
||||
<dt class="col-sm-3">1º Apellido</dt>
|
||||
<dd class="col-sm-9">@denuncia.PrimerApellidoResuelto</dd>
|
||||
}
|
||||
@if (!string.IsNullOrWhiteSpace(denuncia.SegundoApellidoResuelto))
|
||||
{
|
||||
<dt class="col-sm-3">2<EFBFBD> Apellido</dt>
|
||||
<dt class="col-sm-3">2º Apellido</dt>
|
||||
<dd class="col-sm-9">@denuncia.SegundoApellidoResuelto</dd>
|
||||
}
|
||||
@if (!string.IsNullOrWhiteSpace(denuncia.ApellidosResueltos))
|
||||
@@ -322,7 +328,7 @@ else
|
||||
}
|
||||
@if (!string.IsNullOrWhiteSpace(denuncia.PaisOrigen))
|
||||
{
|
||||
<dt class="col-sm-3">Pa<EFBFBD>s de origen</dt>
|
||||
<dt class="col-sm-3">País de origen</dt>
|
||||
<dd class="col-sm-9">@denuncia.PaisOrigen</dd>
|
||||
}
|
||||
</dl>
|
||||
@@ -339,7 +345,7 @@ else
|
||||
<dt class="col-sm-3">Detalle denunciado</dt>
|
||||
<dd class="col-sm-9">@denuncia.DenunciadoDetalle</dd>
|
||||
}
|
||||
<dt class="col-sm-3">Descripci<EFBFBD>n Denuncia</dt>
|
||||
<dt class="col-sm-3">Descripción Denuncia</dt>
|
||||
<dd class="col-sm-9">@denuncia.Descripcion_Denuncia</dd>
|
||||
<dt class="col-sm-3">Denunciado Ante Inst</dt>
|
||||
<dd class="col-sm-9">@denuncia.Denunciado_Ante_Inst</dd>
|
||||
@@ -350,7 +356,7 @@ else
|
||||
}
|
||||
@if (!string.IsNullOrWhiteSpace(denuncia.SolicitaProteccion))
|
||||
{
|
||||
<dt class="col-sm-3">Solicita protecci<EFBFBD>n</dt>
|
||||
<dt class="col-sm-3">Solicita protección</dt>
|
||||
<dd class="col-sm-9">@denuncia.SolicitaProteccion</dd>
|
||||
}
|
||||
@if (!string.IsNullOrWhiteSpace(denuncia.MedidasProteccionSolicitadas))
|
||||
@@ -360,7 +366,7 @@ else
|
||||
}
|
||||
@if (!string.IsNullOrWhiteSpace(denuncia.Modalidad_Informacion))
|
||||
{
|
||||
<dt class="col-sm-3">Modalidad Informaci<EFBFBD>n</dt>
|
||||
<dt class="col-sm-3">Modalidad Información</dt>
|
||||
<dd class="col-sm-9">@denuncia.Modalidad_Informacion</dd>
|
||||
}
|
||||
<dt class="col-sm-3">Lugar Hechos</dt>
|
||||
@@ -372,7 +378,7 @@ else
|
||||
}
|
||||
@if (!string.IsNullOrWhiteSpace(denuncia.AutorizaRemision))
|
||||
{
|
||||
<dt class="col-sm-3">Autoriza remisi<EFBFBD>n</dt>
|
||||
<dt class="col-sm-3">Autoriza remisión</dt>
|
||||
<dd class="col-sm-9">@denuncia.AutorizaRemision</dd>
|
||||
}
|
||||
@if (!string.IsNullOrWhiteSpace(denuncia.PreferenciaRemision))
|
||||
@@ -382,16 +388,16 @@ else
|
||||
}
|
||||
</dl>
|
||||
|
||||
<h5 class="section-heading">Datos de Notificaci<EFBFBD>n</h5>
|
||||
<h5 class="section-heading">Datos de Notificación</h5>
|
||||
<dl class="row">
|
||||
@if (!string.IsNullOrWhiteSpace(denuncia.Notificacion_Preferencia))
|
||||
{
|
||||
<dt class="col-sm-3">Notificaci<EFBFBD>n Preferencia</dt>
|
||||
<dt class="col-sm-3">Notificación Preferencia</dt>
|
||||
<dd class="col-sm-9">@denuncia.Notificacion_Preferencia</dd>
|
||||
}
|
||||
@if (!string.IsNullOrWhiteSpace(denuncia.Notificacion_Electronica))
|
||||
{
|
||||
<dt class="col-sm-3">Notificaci<EFBFBD>n Electr<EFBFBD>nica</dt>
|
||||
<dt class="col-sm-3">Notificación Electrónica</dt>
|
||||
<dd class="col-sm-9">@denuncia.Notificacion_Electronica</dd>
|
||||
}
|
||||
@if (!string.IsNullOrWhiteSpace(denuncia.SeguimientoOnline))
|
||||
@@ -406,17 +412,17 @@ else
|
||||
}
|
||||
@if (!string.IsNullOrWhiteSpace(denuncia.Correo_Electronico))
|
||||
{
|
||||
<dt class="col-sm-3">Correo Electr<EFBFBD>nico</dt>
|
||||
<dt class="col-sm-3">Correo Electrónico</dt>
|
||||
<dd class="col-sm-9">@denuncia.Correo_Electronico</dd>
|
||||
}
|
||||
@if (!string.IsNullOrWhiteSpace(denuncia.Notificacion_Sms))
|
||||
{
|
||||
<dt class="col-sm-3">Notificaci<EFBFBD>n SMS</dt>
|
||||
<dt class="col-sm-3">Notificación SMS</dt>
|
||||
<dd class="col-sm-9">@denuncia.Notificacion_Sms</dd>
|
||||
}
|
||||
@if (HasPostalAddress(denuncia))
|
||||
{
|
||||
<dt class="col-sm-3">Direcci<EFBFBD>n postal</dt>
|
||||
<dt class="col-sm-3">Dirección postal</dt>
|
||||
<dd class="col-sm-9">@BuildPostalAddressSummary(denuncia)</dd>
|
||||
}
|
||||
</dl>
|
||||
@@ -426,7 +432,7 @@ else
|
||||
@if (denuncia.Condiciones)
|
||||
{
|
||||
<dt class="col-sm-3">Condiciones</dt>
|
||||
<dd class="col-sm-9">S<EFBFBD></dd>
|
||||
<dd class="col-sm-9">Sí</dd>
|
||||
}
|
||||
@if (!string.IsNullOrWhiteSpace(denuncia.Comments))
|
||||
{
|
||||
@@ -441,14 +447,14 @@ else
|
||||
@if (camposFormulario.Count > 0)
|
||||
{
|
||||
<h5 class="section-heading">Formulario Original</h5>
|
||||
@foreach (var grupoCampos in camposFormulario.GroupBy(field => string.IsNullOrWhiteSpace(field.Section) ? "Sin secci<EFBFBD>n" : field.Section))
|
||||
@foreach (var grupoCampos in camposFormulario.GroupBy(field => string.IsNullOrWhiteSpace(field.Section) ? "Sin sección" : field.Section))
|
||||
{
|
||||
<h6 class="mt-3">@grupoCampos.Key</h6>
|
||||
<dl class="row">
|
||||
@foreach (var campo in grupoCampos)
|
||||
{
|
||||
<dt class="col-sm-4">@campo.Label</dt>
|
||||
<dd class="col-sm-8">@(string.IsNullOrWhiteSpace(campo.Value) ? "<EFBFBD>" : campo.Value)</dd>
|
||||
<dd class="col-sm-8">@(string.IsNullOrWhiteSpace(campo.Value) ? "—" : campo.Value)</dd>
|
||||
}
|
||||
</dl>
|
||||
}
|
||||
@@ -463,7 +469,7 @@ else
|
||||
<th class="seleccionar-col">Subir</th>
|
||||
<th>Nombre</th>
|
||||
<th>Fecha</th>
|
||||
<th>Tama<EFBFBD>o (bytes)</th>
|
||||
<th>Tamaño (bytes)</th>
|
||||
<th>Ver</th>
|
||||
</tr>
|
||||
</thead>
|
||||
@@ -541,7 +547,7 @@ else
|
||||
</div>
|
||||
}
|
||||
|
||||
<h6 class="modal-section-heading">Descripci<EFBFBD>n</h6>
|
||||
<h6 class="modal-section-heading">Descripción</h6>
|
||||
<div class="mb-3">
|
||||
<input type="text"
|
||||
class="form-control"
|
||||
@@ -554,9 +560,9 @@ else
|
||||
<input type="text"
|
||||
class="form-control"
|
||||
@bind="nombreDocumentos"
|
||||
placeholder="Ej.: Gesti<EFBFBD>n AN (lo ver<EFBFBD>s como: Documento Adjunto 1 Gesti<EFBFBD>n AN, ...)" />
|
||||
placeholder="Ej.: Gestión AN (lo verás como: Documento Adjunto 1 Gestión AN, ...)" />
|
||||
<small class="text-muted">
|
||||
Se aplicar<EFBFBD> al subir en modo <strong>individual</strong>. El <em>report.txt</em> se subir<EFBFBD> como <strong>Denuncia</strong>.
|
||||
Se aplicará al subir en modo <strong>individual</strong>. El <em>report.txt</em> se subirá como <strong>Denuncia</strong>.
|
||||
</small>
|
||||
</div>
|
||||
|
||||
@@ -569,7 +575,7 @@ else
|
||||
checked='@(uploadMode == "merge")'
|
||||
@onclick='() => uploadMode = "merge"' />
|
||||
<label class="form-check-label" for="modoMerge">
|
||||
Unir todos los ficheros en un <EFBFBD>nico PDF
|
||||
Unir todos los ficheros en un único PDF
|
||||
</label>
|
||||
</div>
|
||||
<div class="form-check">
|
||||
@@ -593,10 +599,10 @@ else
|
||||
checked='@(selectedGroup == "600")'
|
||||
@onclick='() => selectedGroup = "600"' />
|
||||
<label class="form-check-label" for="grupo600">
|
||||
600. Asuntos Jur<EFBFBD>dicos y Protecci<EFBFBD>n a la Persona Denunciante
|
||||
600. Asuntos Jurídicos y Protección a la Persona Denunciante
|
||||
</label>
|
||||
</div>
|
||||
@* <div class="form-check">
|
||||
<div class="form-check">
|
||||
<input class="form-check-input"
|
||||
type="radio"
|
||||
name="selectedGroup"
|
||||
@@ -604,20 +610,9 @@ else
|
||||
checked='@(selectedGroup == "510")'
|
||||
@onclick='() => selectedGroup = "510"' />
|
||||
<label class="form-check-label" for="grupo510">
|
||||
510. SDI <EFBFBD> Investigaci<EFBFBD>n Entradas
|
||||
510. SDI – Investigación Entradas
|
||||
</label>
|
||||
</div>
|
||||
<div class="form-check">
|
||||
<input class="form-check-input"
|
||||
type="radio"
|
||||
name="selectedGroup"
|
||||
id="grupo700"
|
||||
checked='@(selectedGroup == "700")'
|
||||
@onclick='() => selectedGroup = "700"' />
|
||||
<label class="form-check-label" for="grupo700">
|
||||
700. RESPONSABLE DEL SERVICIO
|
||||
</label>
|
||||
</div> *@
|
||||
|
||||
<!-- DATOS DEL TERCERO -->
|
||||
@{
|
||||
@@ -626,13 +621,13 @@ else
|
||||
<h6 class="modal-section-heading mt-3">Tercero (denunciante)</h6>
|
||||
|
||||
<div class="alert alert-light border mb-3">
|
||||
Los datos del tercero se cargan autom<EFBFBD>ticamente desde la denuncia y no se pueden editar aqu<EFBFBD>.
|
||||
Los datos del tercero se cargan automáticamente desde la denuncia y no se pueden editar aquí.
|
||||
</div>
|
||||
|
||||
@if (modalThirdParty.IsAnonymous)
|
||||
{
|
||||
<div class="alert alert-warning mb-3">
|
||||
Denuncia an<EFBFBD>nima. Se enlazar<EFBFBD> autom<EFBFBD>ticamente el tercero <strong>00000000T</strong>.
|
||||
Denuncia anónima. Se enlazará automáticamente el tercero <strong>00000000T</strong>.
|
||||
</div>
|
||||
}
|
||||
|
||||
@@ -661,7 +656,7 @@ else
|
||||
{
|
||||
<div class="row g-2">
|
||||
<div class="col-12 mb-2">
|
||||
<label class="form-label">Raz<EFBFBD>n social</label>
|
||||
<label class="form-label">Razón social</label>
|
||||
<input class="form-control" value="@GetReadOnlyValue(selectedDenuncias.RazonSocialResuelta)" readonly />
|
||||
</div>
|
||||
</div>
|
||||
@@ -674,11 +669,11 @@ else
|
||||
<input class="form-control" value="@GetReadOnlyValue(selectedDenuncias.NombreResuelto)" readonly />
|
||||
</div>
|
||||
<div class="col-4 mb-2">
|
||||
<label class="form-label">1<EFBFBD> apellido</label>
|
||||
<label class="form-label">1º apellido</label>
|
||||
<input class="form-control" value="@GetReadOnlyValue(selectedDenuncias.PrimerApellidoResuelto)" readonly />
|
||||
</div>
|
||||
<div class="col-4 mb-2">
|
||||
<label class="form-label">2<EFBFBD> apellido</label>
|
||||
<label class="form-label">2º apellido</label>
|
||||
<input class="form-control" value="@GetReadOnlyValue(selectedDenuncias.SegundoApellidoResuelto)" readonly />
|
||||
</div>
|
||||
</div>
|
||||
@@ -688,14 +683,14 @@ else
|
||||
{
|
||||
<div class="row g-2">
|
||||
<div class="col-12 mb-2">
|
||||
<label class="form-label">Direcci<EFBFBD>n postal</label>
|
||||
<label class="form-label">Dirección postal</label>
|
||||
<textarea class="form-control" rows="2" readonly>@BuildPostalAddressSummary(selectedDenuncias)</textarea>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
<small class="text-muted">
|
||||
Al confirmar, se enlazar<EFBFBD> en Gestiona el tercero obtenido del formulario de la denuncia.
|
||||
Al confirmar, se enlazará en Gestiona el tercero obtenido del formulario de la denuncia.
|
||||
</small>
|
||||
|
||||
@if (!modalThirdParty.IsAnonymous && !string.IsNullOrWhiteSpace(modalThirdParty.DocumentId))
|
||||
@@ -768,7 +763,7 @@ else
|
||||
{
|
||||
<div class="d-flex align-items-center">
|
||||
<div class="spinner-border spinner-border-sm me-2" role="status"></div>
|
||||
<span>Cargando expedientes<EFBFBD></span>
|
||||
<span>Cargando expedientes…</span>
|
||||
</div>
|
||||
}
|
||||
else if (!string.IsNullOrWhiteSpace(errorExpedientes))
|
||||
@@ -788,7 +783,7 @@ else
|
||||
<tr>
|
||||
<th>Expediente</th>
|
||||
<th>Asunto</th>
|
||||
<th>Fecha creaci<EFBFBD>n</th>
|
||||
<th>Fecha creación</th>
|
||||
<th>Estado</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
@@ -827,7 +822,7 @@ else
|
||||
private string nombreDocumentos = string.Empty;
|
||||
private bool isUploading = false;
|
||||
|
||||
private string uploadMode = "merge";
|
||||
private string uploadMode = "individual";
|
||||
private string selectedGroup = "600";
|
||||
|
||||
|
||||
@@ -870,14 +865,6 @@ else
|
||||
{
|
||||
loadError = string.Empty;
|
||||
var todas = await CargarDenunciasJsonAsync();
|
||||
|
||||
// Asegura ProcedureId/GroupId por si faltan
|
||||
foreach (var d in todas.Where(x => x.ProcedureId == Guid.Empty))
|
||||
{
|
||||
d.ProcedureId = Guid.Parse("82722c9b-cecc-4299-8a7b-ce5abeb8170b");
|
||||
d.GroupId = Guid.Parse("6dbfc433-1eb6-4b9a-a533-bfebc652c101");
|
||||
}
|
||||
|
||||
// SOLO pendientes
|
||||
pendientes = todas
|
||||
.Where(d => !d.EnGestiona && !d.EnRechazada && !d.EsActualizacion)
|
||||
@@ -944,6 +931,15 @@ else
|
||||
return new string(clean.Where(ch => ch <= 127).ToArray());
|
||||
}
|
||||
|
||||
private string BuildMergedAttachmentsFileName(int denunciaId, DateTime timestampUtc)
|
||||
{
|
||||
var baseName = string.IsNullOrWhiteSpace(nombreDocumentos)
|
||||
? $"Adjuntos {denunciaId}_{timestampUtc:yyyyMMddHHmmss}"
|
||||
: nombreDocumentos.Trim();
|
||||
|
||||
return FixFileName($"{Path.GetFileNameWithoutExtension(baseName)}.pdf");
|
||||
}
|
||||
|
||||
private async Task ConfirmarEnvio()
|
||||
{
|
||||
if (selectedDenuncias == null) return;
|
||||
@@ -956,6 +952,11 @@ else
|
||||
return;
|
||||
}
|
||||
|
||||
if (!await ConfirmSelectedGroupAsync())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
isUploading = true;
|
||||
@@ -988,10 +989,11 @@ else
|
||||
{
|
||||
operationError = ficherosVacios.Count == 0
|
||||
? $"La denuncia #{selectedDenuncias.Id_Denuncia} no tiene ficheros para subir."
|
||||
: $"La denuncia #{selectedDenuncias.Id_Denuncia} solo tiene ficheros vac<EFBFBD>os y no se ha subido nada. Ficheros omitidos: {string.Join(", ", ficherosVacios)}.";
|
||||
: $"La denuncia #{selectedDenuncias.Id_Denuncia} solo tiene ficheros vacíos y no se ha subido nada. Ficheros omitidos: {string.Join(", ", ficherosVacios)}.";
|
||||
return;
|
||||
}
|
||||
|
||||
var expedienteCreadoEnGestiona = false;
|
||||
string fileUrl;
|
||||
if (selectedDenuncias.EnGestiona && !string.IsNullOrWhiteSpace(selectedDenuncias.Expediente_Gestiona))
|
||||
{
|
||||
@@ -1001,7 +1003,6 @@ else
|
||||
{
|
||||
Busy.Update(message: "Creando expediente nuevo en Gestiona.", detail: "Paso 2 de 7");
|
||||
var createdFile = await ApiDenuncias.CreateGestionaFileAsync(
|
||||
selectedDenuncias.ProcedureId,
|
||||
nuevoAsunto,
|
||||
"RQ2ZLC - Expediente de Denuncias",
|
||||
"3109963"
|
||||
@@ -1011,20 +1012,13 @@ else
|
||||
await ApiDenuncias.OpenGestionaFileAsync(
|
||||
fileUrl,
|
||||
createdFile.FileOpenUrl,
|
||||
managementUnitGroupId: Guid.Parse("a4ad4dfb-70dc-4219-8ee3-4dcc939f0955"),
|
||||
assignedGroupId: selectedGroup switch
|
||||
{
|
||||
"510" => Guid.Parse("6dbfc433-1eb6-4b9a-a533-bfebc652c101"),
|
||||
"600" => Guid.Parse("454fa4ec-8b82-4240-9419-113f45d4b004"),
|
||||
"700" => Guid.Parse("a4ad4dfb-70dc-4219-8ee3-4dcc939f0955"),
|
||||
_ => throw new InvalidOperationException($"Grupo desconocido: {selectedGroup}")
|
||||
},
|
||||
assignedGroupCode: selectedGroup,
|
||||
confidential: selectedDenuncias.Confidencial,
|
||||
freeTitle: nuevoAsunto,
|
||||
siaCode: "3109963"
|
||||
freeTitle: nuevoAsunto
|
||||
);
|
||||
selectedDenuncias.Expediente_Gestiona = fileUrl;
|
||||
selectedDenuncias.EnGestiona = true;
|
||||
expedienteCreadoEnGestiona = true;
|
||||
}
|
||||
|
||||
Busy.Update(message: "Guardando referencia local del expediente.", detail: "Paso 3 de 7");
|
||||
@@ -1033,7 +1027,7 @@ else
|
||||
|
||||
var thirdParty = selectedThirdParty ?? ThirdPartyIdentityData.FromComplaint(selectedDenuncias);
|
||||
Busy.Update(message: "Resolviendo tercero y enlazandolo al expediente.", detail: "Paso 4 de 7");
|
||||
await ApiDenuncias.EnsureGestionaThirdAndLinkAsync(fileUrl, thirdParty);
|
||||
var thirdResult = await ApiDenuncias.EnsureGestionaThirdAndLinkAsync(fileUrl, thirdParty);
|
||||
|
||||
var nombresOriginalesSubidos = new List<string>();
|
||||
var nombresFinalesSubidos = new List<string>();
|
||||
@@ -1045,8 +1039,8 @@ else
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(report.FileName))
|
||||
{
|
||||
Busy.Update(message: "Preparando y subiendo el report para firma.", detail: "Paso 5 de 7");
|
||||
var reportPdfBytes = PdfHelper.MergeFilesToPdf(new[]
|
||||
Busy.Update(message: "Preparando y subiendo el documento principal.", detail: "Paso 5 de 7");
|
||||
var reportPdfBytes = PdfHelper.MergeReportToPdf(new[]
|
||||
{
|
||||
(FileName: report.FileName, Content: report.Content)
|
||||
});
|
||||
@@ -1067,7 +1061,7 @@ else
|
||||
{
|
||||
Busy.Update(message: "Uniendo adjuntos en un unico PDF y subiendolo.", detail: "Paso 6 de 7");
|
||||
var pdfBytes = PdfHelper.MergeFilesToPdf(adjuntos);
|
||||
var pdfName = FixFileName($"Adjuntos {selectedDenuncias.Id_Denuncia}_{ahoraUtc:yyyyMMddHHmmss}.pdf");
|
||||
var pdfName = BuildMergedAttachmentsFileName(selectedDenuncias.Id_Denuncia, ahoraUtc);
|
||||
var docUrl = await ApiDenuncias.UploadGestionaDocumentAsync(fileUrl, pdfBytes, pdfName);
|
||||
if (string.IsNullOrWhiteSpace(documentoParaTramitar))
|
||||
{
|
||||
@@ -1127,11 +1121,12 @@ else
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(documentoParaTramitar))
|
||||
{
|
||||
Busy.Update(message: "Enviando el report al circuito de firma.", detail: "Paso 7 de 7");
|
||||
Busy.Update(message: "Finalizando el documento principal en Gestiona.", detail: "Paso 7 de 7");
|
||||
await ApiDenuncias.TramitarGestionaDocumentAsync(
|
||||
documentoParaTramitar,
|
||||
GetAssignedGroupLinkBySelectedGroup(),
|
||||
selectedDenuncias.Id_Denuncia);
|
||||
selectedGroup,
|
||||
selectedDenuncias.Id_Denuncia,
|
||||
isUpdate: false);
|
||||
}
|
||||
|
||||
foreach (var origName in nombresOriginalesSubidos)
|
||||
@@ -1155,7 +1150,16 @@ else
|
||||
selectedDenuncias.EsActualizacion = false;
|
||||
selectedDenuncias.NombreDenuncia = nuevoAsunto;
|
||||
selectedDenuncias.FechaSubidaAGestiona = ahoraUtc;
|
||||
selectedDenuncias.UltimaSubidaGestionaTipo = "Nueva denuncia";
|
||||
selectedDenuncias.UltimoGrupoAsignadoGestiona = GetGestionaGroupDisplay(selectedGroup);
|
||||
await SincronizarExpedienteGestionaAsync(selectedDenuncias, fileUrl);
|
||||
await ActualizarDenunciaAsync(selectedDenuncias);
|
||||
var historialAviso = await RegistrarHistorialGestionaAsync(
|
||||
selectedDenuncias,
|
||||
"Nueva denuncia",
|
||||
selectedGroup,
|
||||
ahoraUtc,
|
||||
string.Join("; ", nombresFinalesSubidos));
|
||||
|
||||
var denunciaProcesadaId = selectedDenuncias.Id_Denuncia;
|
||||
pendientes.Remove(selectedDenuncias);
|
||||
@@ -1165,22 +1169,35 @@ else
|
||||
var avisos = new List<string>();
|
||||
if (ficherosVacios.Count > 0)
|
||||
{
|
||||
avisos.Add($"Se omitieron ficheros vac<EFBFBD>os: {string.Join(", ", ficherosVacios)}.");
|
||||
avisos.Add($"Se omitieron ficheros vacíos: {string.Join(", ", ficherosVacios)}.");
|
||||
}
|
||||
if (ficherosNoSeleccionados.Count > 0)
|
||||
{
|
||||
avisos.Add($"No se subieron por selecci<EFBFBD>n del usuario: {string.Join(", ", ficherosNoSeleccionados)}.");
|
||||
avisos.Add($"No se subieron por selección del usuario: {string.Join(", ", ficherosNoSeleccionados)}.");
|
||||
}
|
||||
if (avisos.Count > 0)
|
||||
if (thirdResult.Warnings.Count > 0)
|
||||
{
|
||||
operationNotice = $"Denuncia #{denunciaProcesadaId} enviada. {string.Join(" ", avisos)}";
|
||||
avisos.AddRange(thirdResult.Warnings);
|
||||
}
|
||||
if (!string.IsNullOrWhiteSpace(historialAviso))
|
||||
{
|
||||
avisos.Add(historialAviso);
|
||||
}
|
||||
var expedienteInfo = string.IsNullOrWhiteSpace(selectedDenuncias.ExpedienteGestionaMostrable)
|
||||
? string.Empty
|
||||
: $" Nº expediente Gestiona: {selectedDenuncias.ExpedienteGestionaMostrable}.";
|
||||
var resumen = expedienteCreadoEnGestiona
|
||||
? $"Denuncia #{denunciaProcesadaId}: se ha creado el expediente en Gestiona y se han subido los documentos.{expedienteInfo}"
|
||||
: $"Denuncia #{denunciaProcesadaId}: se han subido los documentos al expediente de Gestiona.{expedienteInfo}";
|
||||
operationNotice = avisos.Count > 0
|
||||
? $"{resumen} {string.Join(" ", avisos)}"
|
||||
: resumen;
|
||||
StateHasChanged();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.Error.WriteLine($"Error al confirmar env<EFBFBD>o: {ex}");
|
||||
operationError = $"No se ha podido completar el env<EFBFBD>o de la denuncia #{selectedDenuncias?.Id_Denuncia}: {ex.Message}";
|
||||
Console.Error.WriteLine($"Error al confirmar envío: {ex}");
|
||||
operationError = $"No se ha podido completar el envío de la denuncia #{selectedDenuncias?.Id_Denuncia}: {ex.Message}";
|
||||
}
|
||||
finally
|
||||
{
|
||||
@@ -1226,15 +1243,37 @@ else
|
||||
}
|
||||
}
|
||||
|
||||
private string GetAssignedGroupLinkBySelectedGroup()
|
||||
private static string GetGestionaGroupDisplay(string? groupCode)
|
||||
{
|
||||
return selectedGroup switch
|
||||
return string.IsNullOrWhiteSpace(groupCode) ? string.Empty : groupCode.Trim();
|
||||
}
|
||||
|
||||
private async Task<string?> RegistrarHistorialGestionaAsync(
|
||||
DenunciasGestiona denuncia,
|
||||
string tipoSubida,
|
||||
string grupo,
|
||||
DateTime uploadedAtUtc,
|
||||
string documentos)
|
||||
{
|
||||
"510" => "https://02.g3stiona.com/rest/groups/6dbfc433-1eb6-4b9a-a533-bfebc652c101",
|
||||
"600" => "https://02.g3stiona.com/rest/groups/454fa4ec-8b82-4240-9419-113f45d4b004",
|
||||
"700" => "https://02.g3stiona.com/rest/groups/a4ad4dfb-70dc-4219-8ee3-4dcc939f0955",
|
||||
_ => throw new InvalidOperationException($"Grupo desconocido: {selectedGroup}")
|
||||
};
|
||||
try
|
||||
{
|
||||
await ApiDenuncias.RegisterGestionaUploadHistoryAsync(
|
||||
new GestionaUploadHistoryCreateRequest(
|
||||
denuncia.Id_Denuncia,
|
||||
denuncia.Expediente_Gestiona ?? string.Empty,
|
||||
denuncia.ExpedienteGestionaMostrable,
|
||||
tipoSubida,
|
||||
grupo,
|
||||
uploadedAtUtc,
|
||||
denuncia.NombreDenuncia ?? string.Empty,
|
||||
documentos));
|
||||
return null;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.Error.WriteLine($"No se ha podido registrar el historico de subida a Gestiona: {ex}");
|
||||
return "La subida se ha completado, pero no se ha podido registrar el histórico operativo en la aplicación.";
|
||||
}
|
||||
}
|
||||
|
||||
private async Task ConfirmarRechazo()
|
||||
@@ -1258,8 +1297,13 @@ else
|
||||
await DenunciaStore.UpsertDenunciaAsync(d);
|
||||
}
|
||||
|
||||
private void OpenEnviarAGestionaModal(DenunciasGestiona d)
|
||||
private async Task OpenEnviarAGestionaModal(DenunciasGestiona d)
|
||||
{
|
||||
if (!await ConfirmDifferentOwnerAsync(d, "tramitar"))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
selectedDenuncias = d;
|
||||
nuevoAsunto = $"Denuncia {d.Id_Denuncia}-CD";
|
||||
|
||||
@@ -1271,13 +1315,64 @@ else
|
||||
showModal = true;
|
||||
}
|
||||
|
||||
private void OpenRechazarModal(DenunciasGestiona d)
|
||||
private async Task OpenRechazarModal(DenunciasGestiona d)
|
||||
{
|
||||
if (!await ConfirmDifferentOwnerAsync(d, "rechazar"))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
selectedDenuncias = d;
|
||||
motivoRechazo = string.Empty;
|
||||
showModalRechazo = true;
|
||||
}
|
||||
|
||||
private async Task<bool> ConfirmDifferentOwnerAsync(
|
||||
DenunciasGestiona denuncia,
|
||||
string action)
|
||||
{
|
||||
if (!denuncia.RequiresOwnerConfirmation)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
var confirmText = string.Equals(action, "rechazar", StringComparison.OrdinalIgnoreCase)
|
||||
? "Continuar con el rechazo"
|
||||
: "Continuar con la apertura";
|
||||
|
||||
return await Dialogs.ConfirmAsync(
|
||||
"Denuncia asignada a otro usuario",
|
||||
$"La denuncia #{denuncia.Id_Denuncia} está asignada a {denuncia.OwnerUsername}. " +
|
||||
$"Puedes {action}la porque pertenece a un usuario de tu grupo. La acción quedará registrada con tu usuario.",
|
||||
confirmText);
|
||||
}
|
||||
|
||||
private async Task<bool> ConfirmSelectedGroupAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
var currentGroups = await ApiDenuncias.GetCurrentUserWorkGroupsAsync();
|
||||
if (currentGroups.GroupCodes.Contains(selectedGroup, StringComparer.OrdinalIgnoreCase))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
var configuredGroups = currentGroups.GroupCodes.Count == 0
|
||||
? "ningún grupo"
|
||||
: string.Join(", ", currentGroups.GroupCodes);
|
||||
return await Dialogs.ConfirmAsync(
|
||||
"Cambio de asignación en Gestiona",
|
||||
$"El grupo de destino {selectedGroup} no pertenece a tus grupos configurados ({configuredGroups}). " +
|
||||
"Si continúas, la denuncia quedará asignada en Gestiona a un grupo distinto de los tuyos.",
|
||||
"Cambiar asignación");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
operationError = $"No se han podido comprobar tus grupos antes de la subida: {ex.Message}";
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private void CloseModal()
|
||||
{
|
||||
showModal = false;
|
||||
@@ -1379,7 +1474,7 @@ else
|
||||
}
|
||||
|
||||
private static string GetReadOnlyValue(string? value) =>
|
||||
string.IsNullOrWhiteSpace(value) ? "<EFBFBD>" : value;
|
||||
string.IsNullOrWhiteSpace(value) ? "—" : value;
|
||||
|
||||
private static bool HasPostalAddress(DenunciasGestiona denuncia)
|
||||
{
|
||||
@@ -1438,18 +1533,7 @@ else
|
||||
return string.Join(" | ", parts);
|
||||
}
|
||||
|
||||
// mover a Actualizaciones
|
||||
private async Task MoverAActualizaciones(DenunciasGestiona d)
|
||||
{
|
||||
d.EsActualizacion = true;
|
||||
d.EnRechazada = false;
|
||||
await ActualizarDenunciaAsync(d);
|
||||
|
||||
pendientes.RemoveAll(x => x.Id_Denuncia == d.Id_Denuncia);
|
||||
StateHasChanged();
|
||||
}
|
||||
|
||||
// ========= L<>GICA BUSCADOR DE EXPEDIENTES POR TERCERO =========
|
||||
// ========= LÓGICA BUSCADOR DE EXPEDIENTES POR TERCERO =========
|
||||
|
||||
private void CloseExpedientesModal()
|
||||
{
|
||||
@@ -1468,7 +1552,7 @@ else
|
||||
|
||||
if (string.IsNullOrWhiteSpace(nif) || nif == "00000000T")
|
||||
{
|
||||
errorExpedientes = "NIF no v<EFBFBD>lido para b<EFBFBD>squeda (an<EFBFBD>nimo o vac<EFBFBD>o).";
|
||||
errorExpedientes = "NIF no válido para búsqueda (anónimo o vacío).";
|
||||
showExpedientesModal = true;
|
||||
StateHasChanged();
|
||||
return;
|
||||
@@ -1499,3 +1583,4 @@ else
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -52,6 +52,7 @@ builder.Services.AddScoped<UserState>();
|
||||
builder.Services.AddSingleton<AppSessionLifetime>();
|
||||
builder.Services.AddSingleton<LoginRateLimiter>();
|
||||
builder.Services.AddScoped<UiBusyService>();
|
||||
builder.Services.AddScoped<UiDialogService>();
|
||||
builder.Services.AddScoped<ApiDenunciasClient>();
|
||||
builder.Services.AddScoped<IDenunciaStore, ApiDenunciaStore>();
|
||||
builder.Services.AddScoped<IInboxTrackingService, ApiInboxTrackingService>();
|
||||
|
||||
@@ -34,6 +34,9 @@ public sealed class ApiDenunciaStore : IDenunciaStore
|
||||
public async Task<List<FicherosDenuncias>> GetFicherosByDenunciaAsync(int denunciaId, CancellationToken cancellationToken = default)
|
||||
=> (await _api.GetAsync<List<FicherosDenuncias>>($"api/denuncias/{denunciaId}/ficheros", cancellationToken)) ?? [];
|
||||
|
||||
public async Task<List<GestionaUploadHistoryEntry>> GetGestionaUploadHistoryAsync(CancellationToken cancellationToken = default)
|
||||
=> (await _api.GetAsync<List<GestionaUploadHistoryEntry>>("api/denuncias/gestiona-history", cancellationToken)) ?? [];
|
||||
|
||||
public Task<DenunciasGestiona?> GetDenunciaByIdAsync(int denunciaId, CancellationToken cancellationToken = default)
|
||||
=> _api.GetAsync<DenunciasGestiona?>($"api/denuncias/{denunciaId}", cancellationToken);
|
||||
|
||||
@@ -43,6 +46,12 @@ public sealed class ApiDenunciaStore : IDenunciaStore
|
||||
public Task UpsertFicherosAsync(IEnumerable<FicherosDenuncias> ficheros, CancellationToken cancellationToken = default)
|
||||
=> _api.PostAsync("api/denuncias/ficheros", new UpsertFicherosRequest(ficheros.ToArray()), cancellationToken);
|
||||
|
||||
public Task AddGestionaUploadHistoryAsync(
|
||||
GestionaUploadHistoryCreateRequest request,
|
||||
string username,
|
||||
CancellationToken cancellationToken = default)
|
||||
=> _api.PostAsync("api/denuncias/gestiona-history", request, cancellationToken);
|
||||
|
||||
public Task MarkFicherosAsUploadedAsync(
|
||||
int denunciaId,
|
||||
IEnumerable<string> fileNames,
|
||||
|
||||
@@ -47,6 +47,15 @@ public sealed class ApiDenunciasClient
|
||||
public Task<ApiGlobalLeaksSessionDto?> GetGlobalLeaksSessionAsync(CancellationToken cancellationToken = default)
|
||||
=> SendAsync<ApiGlobalLeaksSessionDto?>(HttpMethod.Get, "api/inbox/session", body: null, authorize: true, cancellationToken, allowNull: true);
|
||||
|
||||
public Task<ApiGlobalLeaksSessionDto?> KeepGlobalLeaksSessionAliveAsync(CancellationToken cancellationToken = default)
|
||||
=> SendAsync<ApiGlobalLeaksSessionDto?>(
|
||||
HttpMethod.Post,
|
||||
"api/inbox/session/keepalive",
|
||||
body: null,
|
||||
authorize: true,
|
||||
cancellationToken,
|
||||
allowNull: true);
|
||||
|
||||
public Task<ApiLoginPrepareResponse> PrepareGlobalLeaksSessionRenewalAsync(CancellationToken cancellationToken = default)
|
||||
=> SendAsync<ApiLoginPrepareResponse>(
|
||||
HttpMethod.Post,
|
||||
@@ -72,11 +81,14 @@ public sealed class ApiDenunciasClient
|
||||
public Task<InboxSnapshotResponse> LoadInboxAsync(CancellationToken cancellationToken = default)
|
||||
=> SendAsync<InboxSnapshotResponse>(HttpMethod.Get, "api/inbox/reports", body: null, authorize: true, cancellationToken);
|
||||
|
||||
public Task<ImportSummary> ImportReportAsync(ReportDto report, CancellationToken cancellationToken = default)
|
||||
public Task<ImportSummary> ImportReportAsync(
|
||||
ReportDto report,
|
||||
bool confirmDifferentOwner = false,
|
||||
CancellationToken cancellationToken = default)
|
||||
=> SendAsync<ImportSummary>(
|
||||
HttpMethod.Post,
|
||||
$"api/inbox/reports/{Uri.EscapeDataString(report.Id)}/import",
|
||||
new ImportReportRequest(report),
|
||||
new ImportReportRequest(report, confirmDifferentOwner),
|
||||
authorize: true,
|
||||
cancellationToken);
|
||||
|
||||
@@ -99,46 +111,35 @@ public sealed class ApiDenunciasClient
|
||||
cancellationToken);
|
||||
}
|
||||
|
||||
public Task EnsureStorageReadyAsync(CancellationToken cancellationToken = default)
|
||||
=> SendAsync<object?>(HttpMethod.Post, "api/inbox/local/ensure-storage", body: null, authorize: true, cancellationToken);
|
||||
|
||||
public Task<ImportSummary> ProcessLocalZipsAsync(CancellationToken cancellationToken = default)
|
||||
=> SendAsync<ImportSummary>(HttpMethod.Post, "api/inbox/local/process", body: null, authorize: true, cancellationToken);
|
||||
|
||||
public Task<IReadOnlyList<string>> GetExistingZipNamesAsync(CancellationToken cancellationToken = default)
|
||||
=> SendAsync<IReadOnlyList<string>>(HttpMethod.Get, "api/inbox/local/zips", body: null, authorize: true, cancellationToken);
|
||||
|
||||
public Task DeleteZipAsync(string zipName, CancellationToken cancellationToken = default)
|
||||
=> SendAsync<object?>(
|
||||
HttpMethod.Delete,
|
||||
$"api/inbox/local/zips/{Uri.EscapeDataString(zipName)}",
|
||||
body: null,
|
||||
authorize: true,
|
||||
cancellationToken);
|
||||
|
||||
public Task<GestionaCreateFileResponse> CreateGestionaFileAsync(
|
||||
Guid procedureId,
|
||||
string subject,
|
||||
string documentSeries,
|
||||
string siaCode,
|
||||
CancellationToken cancellationToken = default)
|
||||
=> PostAsync<GestionaCreateFileResponse>(
|
||||
"api/gestiona/files",
|
||||
new GestionaCreateFileRequest(procedureId, subject, documentSeries, siaCode),
|
||||
new GestionaCreateFileRequest(subject, documentSeries, siaCode),
|
||||
cancellationToken);
|
||||
|
||||
public Task OpenGestionaFileAsync(
|
||||
string fileUrl,
|
||||
string? fileOpenUrl,
|
||||
Guid managementUnitGroupId,
|
||||
Guid assignedGroupId,
|
||||
string assignedGroupCode,
|
||||
bool confidential,
|
||||
string freeTitle,
|
||||
string siaCode,
|
||||
CancellationToken cancellationToken = default)
|
||||
=> PostAsync(
|
||||
"api/gestiona/files/open",
|
||||
new GestionaOpenFileRequest(fileUrl, fileOpenUrl, managementUnitGroupId, assignedGroupId, confidential, freeTitle, siaCode),
|
||||
new GestionaOpenFileRequest(fileUrl, fileOpenUrl, assignedGroupCode, confidential, freeTitle),
|
||||
cancellationToken);
|
||||
|
||||
public Task AssignGestionaFileAsync(
|
||||
string fileUrl,
|
||||
string assignedGroupCode,
|
||||
CancellationToken cancellationToken = default)
|
||||
=> PostAsync(
|
||||
"api/gestiona/files/assignees",
|
||||
new GestionaAssignFileRequest(fileUrl, assignedGroupCode),
|
||||
cancellationToken);
|
||||
|
||||
public Task<GestionaExpedienteInfo?> GetGestionaExpedienteAsync(
|
||||
@@ -149,11 +150,27 @@ public sealed class ApiDenunciasClient
|
||||
cancellationToken,
|
||||
allowNull: true);
|
||||
|
||||
public Task EnsureGestionaThirdAndLinkAsync(
|
||||
public Task<GestionaAuditInfo?> GetGestionaFileLatestAuditAsync(
|
||||
string fileUrl,
|
||||
CancellationToken cancellationToken = default)
|
||||
=> GetAsync<GestionaAuditInfo?>(
|
||||
$"api/gestiona/files/audit/latest?fileUrl={Uri.EscapeDataString(fileUrl)}",
|
||||
cancellationToken,
|
||||
allowNull: true);
|
||||
|
||||
public Task<List<GestionaUploadHistoryEntry>> GetGestionaUploadHistoryAsync(CancellationToken cancellationToken = default)
|
||||
=> GetAsync<List<GestionaUploadHistoryEntry>>("api/denuncias/gestiona-history", cancellationToken);
|
||||
|
||||
public Task RegisterGestionaUploadHistoryAsync(
|
||||
GestionaUploadHistoryCreateRequest request,
|
||||
CancellationToken cancellationToken = default)
|
||||
=> PostAsync("api/denuncias/gestiona-history", request, cancellationToken);
|
||||
|
||||
public Task<GestionaEnsureThirdResponse> EnsureGestionaThirdAndLinkAsync(
|
||||
string fileUrl,
|
||||
ThirdPartyIdentityData thirdParty,
|
||||
CancellationToken cancellationToken = default)
|
||||
=> PostAsync(
|
||||
=> PostAsync<GestionaEnsureThirdResponse>(
|
||||
"api/gestiona/thirds/ensure-link",
|
||||
new GestionaEnsureThirdRequest(fileUrl, thirdParty),
|
||||
cancellationToken);
|
||||
@@ -183,12 +200,19 @@ public sealed class ApiDenunciasClient
|
||||
|
||||
public Task TramitarGestionaDocumentAsync(
|
||||
string documentUrl,
|
||||
string assignedGroupHref,
|
||||
string assignedGroupCode,
|
||||
int? complaintId,
|
||||
bool isUpdate = false,
|
||||
string? updateSource = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
=> PostAsync(
|
||||
"api/gestiona/documents/tramitar",
|
||||
new GestionaTramitarDocumentoRequest(documentUrl, assignedGroupHref, complaintId),
|
||||
new GestionaTramitarDocumentoRequest(
|
||||
documentUrl,
|
||||
assignedGroupCode,
|
||||
complaintId,
|
||||
isUpdate,
|
||||
updateSource),
|
||||
cancellationToken);
|
||||
|
||||
public Task<List<ExpedienteTerceroDto>> GetGestionaExpedientesPorTerceroAsync(
|
||||
@@ -249,6 +273,29 @@ public sealed class ApiDenunciasClient
|
||||
authorize: true,
|
||||
cancellationToken);
|
||||
|
||||
public Task<WorkGroupAdministrationDto> GetWorkGroupAdministrationAsync(
|
||||
CancellationToken cancellationToken = default)
|
||||
=> GetAsync<WorkGroupAdministrationDto>(
|
||||
"api/configuration/work-groups",
|
||||
cancellationToken);
|
||||
|
||||
public Task<CurrentUserWorkGroupsDto> GetCurrentUserWorkGroupsAsync(
|
||||
CancellationToken cancellationToken = default)
|
||||
=> GetAsync<CurrentUserWorkGroupsDto>(
|
||||
"api/configuration/work-groups/current",
|
||||
cancellationToken);
|
||||
|
||||
public Task<WorkGroupAdministrationDto> UpdateUserWorkGroupsAsync(
|
||||
string username,
|
||||
IReadOnlyList<string> groupCodes,
|
||||
CancellationToken cancellationToken = default)
|
||||
=> SendAsync<WorkGroupAdministrationDto>(
|
||||
HttpMethod.Put,
|
||||
$"api/configuration/work-groups/users/{Uri.EscapeDataString(username)}",
|
||||
new UpdateUserWorkGroupsRequest(groupCodes),
|
||||
authorize: true,
|
||||
cancellationToken);
|
||||
|
||||
internal Task<T> GetAsync<T>(string path, CancellationToken cancellationToken = default, bool allowNull = false)
|
||||
=> SendAsync<T>(HttpMethod.Get, path, body: null, authorize: true, cancellationToken, allowNull);
|
||||
|
||||
@@ -292,13 +339,16 @@ public sealed class ApiDenunciasClient
|
||||
using var response = await client.SendAsync(request, cancellationToken);
|
||||
if (response.StatusCode == HttpStatusCode.Unauthorized)
|
||||
{
|
||||
if (authorize)
|
||||
var message = await ReadErrorMessageAsync(response, cancellationToken);
|
||||
if (!string.IsNullOrWhiteSpace(message) &&
|
||||
!message.StartsWith("La API de denuncias ha respondido con", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
throw new UnauthorizedAccessException($"La sesion de API ha caducado al llamar a {path}. Vuelve a iniciar sesion.");
|
||||
throw new UnauthorizedAccessException(message);
|
||||
}
|
||||
|
||||
var message = await ReadErrorMessageAsync(response, cancellationToken);
|
||||
throw new UnauthorizedAccessException(message);
|
||||
throw new UnauthorizedAccessException(authorize
|
||||
? "La sesion de la aplicacion ha caducado. Vuelve a iniciar sesion."
|
||||
: "No se ha podido autorizar la peticion. Vuelve a iniciar sesion.");
|
||||
}
|
||||
|
||||
if (!response.IsSuccessStatusCode)
|
||||
|
||||
@@ -33,12 +33,23 @@ public sealed class ApiInboxTrackingService : IInboxTrackingService
|
||||
new MarkReportImportedRequest(username, report, complaintId),
|
||||
cancellationToken);
|
||||
|
||||
public Task MarkReportHandledInGestionaAsync(
|
||||
string username,
|
||||
int denunciaId,
|
||||
DateTime uploadedAtUtc,
|
||||
CancellationToken cancellationToken = default)
|
||||
=> _api.PostAsync(
|
||||
"api/tracking/handled-in-gestiona",
|
||||
new MarkReportHandledInGestionaRequest(username, denunciaId, uploadedAtUtc),
|
||||
cancellationToken);
|
||||
|
||||
public Task EnsureReportCanBeImportedByUserAsync(
|
||||
string username,
|
||||
ReportDto report,
|
||||
bool confirmDifferentOwner = false,
|
||||
CancellationToken cancellationToken = default)
|
||||
=> _api.PostAsync(
|
||||
"api/tracking/import-permission",
|
||||
new TrackingImportPermissionRequest(username, report),
|
||||
new TrackingImportPermissionRequest(username, report, confirmDifferentOwner),
|
||||
cancellationToken);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
namespace GestionaDenunciasAN.Services;
|
||||
|
||||
public enum UiDialogTone
|
||||
{
|
||||
Information,
|
||||
Warning,
|
||||
Danger
|
||||
}
|
||||
|
||||
public sealed class UiDialogService
|
||||
{
|
||||
private TaskCompletionSource<bool>? _pendingConfirmation;
|
||||
|
||||
public event Action? Changed;
|
||||
|
||||
public bool IsVisible { get; private set; }
|
||||
public string Title { get; private set; } = string.Empty;
|
||||
public string Message { get; private set; } = string.Empty;
|
||||
public string ConfirmText { get; private set; } = "Continuar";
|
||||
public string CancelText { get; private set; } = "Cancelar";
|
||||
public UiDialogTone Tone { get; private set; } = UiDialogTone.Warning;
|
||||
|
||||
public Task<bool> ConfirmAsync(
|
||||
string title,
|
||||
string message,
|
||||
string confirmText = "Continuar",
|
||||
string cancelText = "Cancelar",
|
||||
UiDialogTone tone = UiDialogTone.Warning)
|
||||
{
|
||||
_pendingConfirmation?.TrySetResult(false);
|
||||
|
||||
Title = title.Trim();
|
||||
Message = message.Trim();
|
||||
ConfirmText = confirmText.Trim();
|
||||
CancelText = cancelText.Trim();
|
||||
Tone = tone;
|
||||
IsVisible = true;
|
||||
|
||||
_pendingConfirmation = new TaskCompletionSource<bool>(
|
||||
TaskCreationOptions.RunContinuationsAsynchronously);
|
||||
|
||||
Changed?.Invoke();
|
||||
return _pendingConfirmation.Task;
|
||||
}
|
||||
|
||||
public void Confirm()
|
||||
{
|
||||
Complete(true);
|
||||
}
|
||||
|
||||
public void Cancel()
|
||||
{
|
||||
Complete(false);
|
||||
}
|
||||
|
||||
private void Complete(bool confirmed)
|
||||
{
|
||||
var pendingConfirmation = _pendingConfirmation;
|
||||
_pendingConfirmation = null;
|
||||
IsVisible = false;
|
||||
Changed?.Invoke();
|
||||
pendingConfirmation?.TrySetResult(confirmed);
|
||||
}
|
||||
}
|
||||
@@ -141,6 +141,13 @@ pre {
|
||||
gap: 0.85rem;
|
||||
}
|
||||
|
||||
.app-status-pills {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: flex-end;
|
||||
gap: 0.65rem;
|
||||
}
|
||||
|
||||
.app-session-pill {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
@@ -160,6 +167,24 @@ pre {
|
||||
box-shadow: 0 0 0 0.2rem rgba(31, 122, 85, 0.12);
|
||||
}
|
||||
|
||||
.app-key-pill {
|
||||
background: rgba(42, 82, 152, 0.1);
|
||||
color: var(--app-accent-strong);
|
||||
}
|
||||
|
||||
.app-key-pill .app-session-pill__dot {
|
||||
box-shadow: 0 0 0 0.2rem rgba(42, 82, 152, 0.12);
|
||||
}
|
||||
|
||||
.app-key-pill--warning {
|
||||
background: rgba(184, 117, 18, 0.14);
|
||||
color: #8a5a12;
|
||||
}
|
||||
|
||||
.app-key-pill--warning .app-session-pill__dot {
|
||||
box-shadow: 0 0 0 0.2rem rgba(184, 117, 18, 0.14);
|
||||
}
|
||||
|
||||
.app-user-chip {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
@@ -548,6 +573,14 @@ h1:focus {
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.app-status-pills {
|
||||
justify-content: stretch;
|
||||
}
|
||||
|
||||
.app-status-pills .app-session-pill {
|
||||
flex: 1 1 14rem;
|
||||
}
|
||||
|
||||
.app-user-chip,
|
||||
.app-session-pill {
|
||||
justify-content: center;
|
||||
|
||||
@@ -68,3 +68,62 @@ window.appSetBodyScrollLock = function (locked) {
|
||||
document.documentElement.classList.toggle("app-scroll-locked", Boolean(locked));
|
||||
document.body.classList.toggle("app-scroll-locked", Boolean(locked));
|
||||
};
|
||||
|
||||
window.appGlobalLeaksActivity = (function () {
|
||||
const storageKey = "gestiona-denuncias:last-user-activity";
|
||||
const events = ["pointerdown", "pointermove", "keydown", "touchstart", "scroll"];
|
||||
let lastActivity = Date.now();
|
||||
let tracking = false;
|
||||
|
||||
function readSharedActivity() {
|
||||
try {
|
||||
const stored = Number(window.localStorage.getItem(storageKey));
|
||||
return Number.isFinite(stored) && stored > 0 ? stored : 0;
|
||||
} catch {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
function markActivity() {
|
||||
lastActivity = Date.now();
|
||||
try {
|
||||
window.localStorage.setItem(storageKey, String(lastActivity));
|
||||
} catch {
|
||||
// The in-memory timestamp still works when local storage is unavailable.
|
||||
}
|
||||
}
|
||||
|
||||
function start() {
|
||||
if (tracking) {
|
||||
return;
|
||||
}
|
||||
|
||||
tracking = true;
|
||||
markActivity();
|
||||
for (const eventName of events) {
|
||||
window.addEventListener(eventName, markActivity, { passive: true });
|
||||
}
|
||||
}
|
||||
|
||||
function stop() {
|
||||
if (!tracking) {
|
||||
return;
|
||||
}
|
||||
|
||||
tracking = false;
|
||||
for (const eventName of events) {
|
||||
window.removeEventListener(eventName, markActivity);
|
||||
}
|
||||
}
|
||||
|
||||
function getIdleMilliseconds() {
|
||||
const latestActivity = Math.max(lastActivity, readSharedActivity());
|
||||
return Math.max(0, Date.now() - latestActivity);
|
||||
}
|
||||
|
||||
return {
|
||||
start,
|
||||
stop,
|
||||
getIdleMilliseconds
|
||||
};
|
||||
})();
|
||||
|
||||
@@ -170,7 +170,8 @@
|
||||
}
|
||||
|
||||
var separator = certLoginBaseUrl.Contains('?') ? "&" : "?";
|
||||
var url = $"{certLoginBaseUrl}{separator}iframe=true&parentOrigin={Uri.EscapeDataString(parentOrigin)}";
|
||||
var url =
|
||||
$"{certLoginBaseUrl}{separator}iframe=true&origen=Registro&parentOrigin={Uri.EscapeDataString(parentOrigin)}";
|
||||
await JSRuntime.InvokeVoidAsync("iniciarSesionConCertificado", url);
|
||||
|
||||
}
|
||||
|
||||
@@ -189,7 +189,7 @@
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label class="lblInput" for="unidad-administrativa">Departamento:</label>
|
||||
<input class="form-control" id="unidad-administrativa" value="@Persona.IDDEPARTAMENTONavigation?.DESCRIPCION" disabled />
|
||||
<input class="form-control" id="unidad-administrativa" value="@Persona.DEPARTAMENTOACTUAL" disabled />
|
||||
</div>
|
||||
@* <div class="col-md-2">
|
||||
<label class="lblInput" for="unidad-administrativa">Unidad administrativa:</label>
|
||||
@@ -301,7 +301,7 @@
|
||||
private List<ENUMERACIONES> listadoMutua = new List<ENUMERACIONES>();
|
||||
private List<ENUMERACIONES> listadoEscala = new List<ENUMERACIONES>();
|
||||
private List<ENUMERACIONES> listadoEspecialidad = new List<ENUMERACIONES>();
|
||||
|
||||
private PUESTOS puestoQueOcupa { get; set; } = new PUESTOS();
|
||||
private HttpClient cliente = new HttpClient();
|
||||
|
||||
private int NumeroTrienios { get; set; }
|
||||
|
||||
@@ -55,7 +55,8 @@ namespace RegistroPersonalAN.Services
|
||||
var client = _httpClientFactory.CreateClient("DefaultClient");
|
||||
using var response = await client.PostAsJsonAsync("Auth/login-cert-proxy", new CertificateProxyLoginRequest
|
||||
{
|
||||
Dni = dni
|
||||
Dni = dni,
|
||||
Origen = "Registro"
|
||||
});
|
||||
|
||||
var responseContent = await response.Content.ReadAsStringAsync();
|
||||
@@ -96,6 +97,7 @@ namespace RegistroPersonalAN.Services
|
||||
public sealed class CertificateProxyLoginRequest
|
||||
{
|
||||
public string Dni { get; set; } = string.Empty;
|
||||
public string Origen { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
public sealed class CertificateProxyLoginResponse
|
||||
|
||||
@@ -46,7 +46,7 @@ namespace SwaggerAntifraude.Controllers
|
||||
|
||||
[AllowAnonymous]
|
||||
[HttpGet("login-cert")]
|
||||
public IActionResult LoginWithCertificateGet([FromQuery] bool iframe = false)
|
||||
public IActionResult LoginWithCertificateGet([FromQuery] bool iframe = false, [FromQuery] string? origen = null)
|
||||
{
|
||||
var clientCert = HttpContext.Connection.ClientCertificate;
|
||||
if (clientCert == null)
|
||||
@@ -56,7 +56,7 @@ namespace SwaggerAntifraude.Controllers
|
||||
if (string.IsNullOrWhiteSpace(dni))
|
||||
return Unauthorized("No se pudo obtener un DNI válido del certificado.");
|
||||
|
||||
var result = AuthenticateCertificateDni(dni);
|
||||
var result = AuthenticateCertificateDni(dni, origen);
|
||||
if (result.Token == null || result.Persona == null)
|
||||
return Unauthorized(result.Error ?? "No se pudo autenticar el certificado.");
|
||||
|
||||
@@ -82,14 +82,14 @@ namespace SwaggerAntifraude.Controllers
|
||||
return BadRequest("Debe indicarse un DNI.");
|
||||
}
|
||||
|
||||
var result = AuthenticateCertificateDni(request.Dni);
|
||||
var result = AuthenticateCertificateDni(request.Dni, request.Origen);
|
||||
if (result.Token == null || result.Persona == null)
|
||||
return Unauthorized(result.Error ?? "No se pudo autenticar el certificado.");
|
||||
|
||||
return Ok(BuildLoginResponse(result.Persona, result.Token));
|
||||
}
|
||||
|
||||
private (string? Token, PERSONAS? Persona, string? Error) AuthenticateCertificateDni(string dni)
|
||||
private (string? Token, PERSONAS? Persona, string? Error) AuthenticateCertificateDni(string dni, string? origen)
|
||||
{
|
||||
using var context = tsGestionAntifraude.NuevoContexto(SoloLectura: true);
|
||||
|
||||
@@ -100,7 +100,11 @@ namespace SwaggerAntifraude.Controllers
|
||||
{
|
||||
return (null, null, "Usuario no encontrado en la base de datos.");
|
||||
}
|
||||
|
||||
if (string.Equals(origen, "Registro", StringComparison.OrdinalIgnoreCase)
|
||||
&& persona.ADMINISTRARPTYREGISTRO != true)
|
||||
{
|
||||
return (null, null, "Usuario no autorizado.");
|
||||
}
|
||||
var jwtToken = GenerateJwtToken(persona);
|
||||
return (jwtToken, persona, null);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
using bdAntifraude.db;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace SwaggerAntifraude.Controllers
|
||||
{
|
||||
[ApiController]
|
||||
[Route("api/[controller]")]
|
||||
public class DIFERENCIAPAGODELEGADOController : GenericoController<DIFERENCIAPAGODELEGADO, int>
|
||||
{
|
||||
public DIFERENCIAPAGODELEGADOController()
|
||||
: base()
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
using bdAntifraude.db;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace SwaggerAntifraude.Controllers
|
||||
{
|
||||
[ApiController]
|
||||
[Route("api/[controller]")]
|
||||
public class HUELGASController : GenericoController<HUELGAS, int>
|
||||
{
|
||||
public HUELGASController()
|
||||
: base()
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
using bdAntifraude.db;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace SwaggerAntifraude.Controllers
|
||||
{
|
||||
[ApiController]
|
||||
[Route("api/[controller]")]
|
||||
public class PERMISOSSINRETRIBUCIONController : GenericoController<PERMISOSSINRETRIBUCION, int>
|
||||
{
|
||||
public PERMISOSSINRETRIBUCIONController()
|
||||
: base()
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -155,6 +155,9 @@ namespace SwaggerAntifraude.Controllers
|
||||
.Include(pue => pue.PUESTOSIDPERSONALNavigation)
|
||||
.ThenInclude(rpt => rpt.IDRPTDESNavigation)
|
||||
.ThenInclude(rpt => rpt.IDUNIDADADMINISTRATIVANavigation)
|
||||
.Include(pue => pue.PUESTOSIDPERSONALNavigation)
|
||||
.ThenInclude(rpt => rpt.IDRPTDESNavigation)
|
||||
.ThenInclude(x => x.IDDEPARTAMENTONavigation)
|
||||
.Include(pue => pue.PUESTOSIDPERSONALNavigation)
|
||||
.Include(pue => pue.EXCEPCIONESPERMISOSIDPERSONANavigation)
|
||||
.ThenInclude(exc=>exc.IDDEPARTAMENTONavigation)
|
||||
@@ -201,6 +204,9 @@ namespace SwaggerAntifraude.Controllers
|
||||
.Include(cp => cp.IDDEPARTAMENTONavigation)
|
||||
.Include(cp => cp.CODIGOMUNICIPIONavigation)
|
||||
.ThenInclude(cpro => cpro.CODIGOPROVINCIANavigation)
|
||||
.Include(pue => pue.PUESTOSIDPERSONALNavigation)
|
||||
.ThenInclude(rpt => rpt.IDRPTDESNavigation)
|
||||
.ThenInclude(x => x.IDDEPARTAMENTONavigation)
|
||||
.AsNoTracking()
|
||||
.FirstOrDefault(p => p.NIF == nif);
|
||||
if (persona == null)
|
||||
|
||||
@@ -3,5 +3,7 @@ namespace SwaggerAntifraude.DTOs
|
||||
public class CertificateProxyLoginDto
|
||||
{
|
||||
public string Dni { get; set; } = string.Empty;
|
||||
public string? Origen { get; set; }
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -42,7 +42,7 @@ namespace bdAntifraude.db
|
||||
|
||||
var puesto = PUESTOSIDPERSONALNavigation?.FirstOrDefault(x => x.IDRPTNavigation?.IDSITUACIONNavigation?.DESCRIPCION == "ACTIVA");
|
||||
|
||||
return puesto?.IDRPTDESNavigation?.DESDEP ?? "";
|
||||
return puesto?.IDRPTDESNavigation?.IDDEPARTAMENTONavigation?.VALORALFABETICOLARGO ?? "";
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user