Compare commits
2 Commits
087e439b54
...
1fec0ae0f5
| Author | SHA1 | Date | |
|---|---|---|---|
| 1fec0ae0f5 | |||
| b6c61238a5 |
@@ -23,6 +23,7 @@
|
|||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<Content Include="Scripts\gestiondenuncias_schema.sql" CopyToOutputDirectory="PreserveNewest" />
|
<Content Include="Scripts\gestiondenuncias_schema.sql" CopyToOutputDirectory="PreserveNewest" />
|
||||||
<Content Include="Scripts\gestiondenuncias_envelope_encryption.sql" CopyToOutputDirectory="PreserveNewest" />
|
<Content Include="Scripts\gestiondenuncias_envelope_encryption.sql" CopyToOutputDirectory="PreserveNewest" />
|
||||||
|
<Content Include="Scripts\gestiondenuncias_gestiona_expediente_excepciones.sql" CopyToOutputDirectory="PreserveNewest" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
@@ -4,14 +4,19 @@ namespace ApiDenuncias.Configuration
|
|||||||
{
|
{
|
||||||
public string ApiBase { get; set; } = null!;
|
public string ApiBase { get; set; } = null!;
|
||||||
public string AccessToken { get; set; } = null!;
|
public string AccessToken { get; set; } = null!;
|
||||||
public string UserLink { get; set; } = null!;
|
public string? ProcedureName { get; set; }
|
||||||
public string GroupLink { get; set; } = null!;
|
public string? ExternalProcedureName { get; set; }
|
||||||
public string Location { get; set; } = null!;
|
public string? ExternalProcedureSiaCode { get; set; }
|
||||||
public string? ExternalProcedureId { get; set; }
|
public string? ManagementUnitGroupCode { get; set; }
|
||||||
public string? CircuitTemplateId { get; set; }
|
public string? CircuitTemplateName { get; set; }
|
||||||
public string? CircuitSignerStampHref { get; set; }
|
public string? CircuitNewComplaintTemplateName { get; set; }
|
||||||
|
public string? CircuitUpdateTemplateName { get; set; }
|
||||||
|
public string? CircuitUpdateSajTemplateName { get; set; }
|
||||||
|
public string? CircuitUpdateSdiTemplateName { get; set; }
|
||||||
public string? CircuitSignerStampTitle { get; set; }
|
public string? CircuitSignerStampTitle { get; set; }
|
||||||
public string? CircuitRecipientGroupHref { get; set; }
|
|
||||||
public string? CircuitVersion { get; set; }
|
public string? CircuitVersion { get; set; }
|
||||||
|
public string? DocumentMetadataLanguage { get; set; }
|
||||||
|
public string? DocumentMetadataType { get; set; }
|
||||||
|
public string? DocumentMetadataSubtype { get; set; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ public sealed class ConfigurationController : ControllerBase
|
|||||||
}
|
}
|
||||||
|
|
||||||
[HttpPut("external-update-cutoff")]
|
[HttpPut("external-update-cutoff")]
|
||||||
|
[Authorize(Policy = "ConfigurationAdministrators")]
|
||||||
public async Task<ActionResult<AppConfigurationDto>> SetExternalUpdateCutoff(
|
public async Task<ActionResult<AppConfigurationDto>> SetExternalUpdateCutoff(
|
||||||
UpdateExternalUpdateCutoffRequest request,
|
UpdateExternalUpdateCutoffRequest request,
|
||||||
CancellationToken cancellationToken)
|
CancellationToken cancellationToken)
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
using System.Globalization;
|
||||||
|
using System.Text;
|
||||||
using ApiDenuncias.Services;
|
using ApiDenuncias.Services;
|
||||||
using GestionaDenuncias.Shared.Models;
|
using GestionaDenuncias.Shared.Models;
|
||||||
using Microsoft.AspNetCore.Authorization;
|
using Microsoft.AspNetCore.Authorization;
|
||||||
@@ -13,15 +15,21 @@ public sealed class DenunciasController : ControllerBase
|
|||||||
private readonly IDenunciaStore _denunciaStore;
|
private readonly IDenunciaStore _denunciaStore;
|
||||||
private readonly IFilteredDenunciaStore _filteredDenunciaStore;
|
private readonly IFilteredDenunciaStore _filteredDenunciaStore;
|
||||||
private readonly UserComplaintAccessService _accessService;
|
private readonly UserComplaintAccessService _accessService;
|
||||||
|
private readonly IInboxTrackingService _trackingService;
|
||||||
|
private readonly ILogger<DenunciasController> _logger;
|
||||||
|
|
||||||
public DenunciasController(
|
public DenunciasController(
|
||||||
IDenunciaStore denunciaStore,
|
IDenunciaStore denunciaStore,
|
||||||
IFilteredDenunciaStore filteredDenunciaStore,
|
IFilteredDenunciaStore filteredDenunciaStore,
|
||||||
UserComplaintAccessService accessService)
|
UserComplaintAccessService accessService,
|
||||||
|
IInboxTrackingService trackingService,
|
||||||
|
ILogger<DenunciasController> logger)
|
||||||
{
|
{
|
||||||
_denunciaStore = denunciaStore;
|
_denunciaStore = denunciaStore;
|
||||||
_filteredDenunciaStore = filteredDenunciaStore;
|
_filteredDenunciaStore = filteredDenunciaStore;
|
||||||
_accessService = accessService;
|
_accessService = accessService;
|
||||||
|
_trackingService = trackingService;
|
||||||
|
_logger = logger;
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpPost("schema/ensure")]
|
[HttpPost("schema/ensure")]
|
||||||
@@ -56,6 +64,25 @@ public sealed class DenunciasController : ControllerBase
|
|||||||
return Ok(await _denunciaStore.GetDenunciaByIdAsync(denunciaId, cancellationToken));
|
return Ok(await _denunciaStore.GetDenunciaByIdAsync(denunciaId, cancellationToken));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[HttpGet("{denunciaId:int}/gestiona-fields")]
|
||||||
|
public async Task<ActionResult<GestionaComplaintFieldsResponse>> 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 Ok(ToGestionaComplaintFields(denuncia));
|
||||||
|
}
|
||||||
|
|
||||||
[HttpPost]
|
[HttpPost]
|
||||||
public async Task<IActionResult> Upsert(DenunciasGestiona denuncia, CancellationToken cancellationToken)
|
public async Task<IActionResult> Upsert(DenunciasGestiona denuncia, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
@@ -65,6 +92,7 @@ public sealed class DenunciasController : ControllerBase
|
|||||||
}
|
}
|
||||||
|
|
||||||
await _denunciaStore.UpsertDenunciaAsync(denuncia, cancellationToken);
|
await _denunciaStore.UpsertDenunciaAsync(denuncia, cancellationToken);
|
||||||
|
await TryRegisterGestionaHistoryFromComplaintAsync(denuncia, cancellationToken);
|
||||||
return Ok(new { ok = true });
|
return Ok(new { ok = true });
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -91,6 +119,32 @@ public sealed class DenunciasController : ControllerBase
|
|||||||
return Ok(await _denunciaStore.GetFicherosByDenunciaAsync(denunciaId, cancellationToken));
|
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")]
|
[HttpGet("{denunciaId:int}/ficheros/content")]
|
||||||
public async Task<IActionResult> GetFicheroContent(
|
public async Task<IActionResult> GetFicheroContent(
|
||||||
int denunciaId,
|
int denunciaId,
|
||||||
@@ -178,6 +232,243 @@ public sealed class DenunciasController : ControllerBase
|
|||||||
private string GetUsername()
|
private string GetUsername()
|
||||||
=> User.Identity?.Name ?? throw new InvalidOperationException("No hay usuario autenticado.");
|
=> User.Identity?.Name ?? throw new InvalidOperationException("No hay usuario autenticado.");
|
||||||
|
|
||||||
|
private static GestionaComplaintFieldsResponse ToGestionaComplaintFields(DenunciasGestiona denuncia)
|
||||||
|
{
|
||||||
|
var preferenciaNotificacion = ResolveReportField(
|
||||||
|
denuncia,
|
||||||
|
denuncia.Notificacion_Preferencia,
|
||||||
|
"preferencia de notificacion",
|
||||||
|
"seleccione su preferencia de notificacion y seguimiento de su denuncia",
|
||||||
|
"notificaciones");
|
||||||
|
|
||||||
|
var seguimiento = ResolveReportField(
|
||||||
|
denuncia,
|
||||||
|
denuncia.SeguimientoOnline,
|
||||||
|
"seguimiento online",
|
||||||
|
"seguimiento de su 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: 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"),
|
||||||
|
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: 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"),
|
||||||
|
PreferenciaNotificacionSeguimientoDenuncia: JoinDistinct(preferenciaNotificacion, seguimiento));
|
||||||
|
}
|
||||||
|
|
||||||
|
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 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
return denuncia.Fecha_Hechos == DateTime.MinValue
|
||||||
|
? string.Empty
|
||||||
|
: denuncia.Fecha_Hechos.ToString("dd/MM/yyyy", CultureInfo.InvariantCulture);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string ResolveAmbitoCompetencias(DenunciasGestiona denuncia)
|
||||||
|
{
|
||||||
|
var directValue = ResolveReportField(
|
||||||
|
denuncia,
|
||||||
|
denuncia.Modalidad_Informacion,
|
||||||
|
"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 string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
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 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
private static string GetAttachmentContentType(string? fileName)
|
||||||
{
|
{
|
||||||
return Path.GetExtension(fileName ?? string.Empty).ToLowerInvariant() switch
|
return Path.GetExtension(fileName ?? string.Empty).ToLowerInvariant() switch
|
||||||
|
|||||||
@@ -29,7 +29,6 @@ public sealed class GestionaController : ControllerBase
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
var file = await _gestiona.CreateFileAsync(
|
var file = await _gestiona.CreateFileAsync(
|
||||||
request.ProcedureId,
|
|
||||||
request.Subject,
|
request.Subject,
|
||||||
request.DocumentSeries,
|
request.DocumentSeries,
|
||||||
request.SiaCode);
|
request.SiaCode);
|
||||||
@@ -52,11 +51,28 @@ public sealed class GestionaController : ControllerBase
|
|||||||
await _gestiona.OpenFileAsync(
|
await _gestiona.OpenFileAsync(
|
||||||
request.FileUrl,
|
request.FileUrl,
|
||||||
request.FileOpenUrl,
|
request.FileOpenUrl,
|
||||||
request.ManagementUnitGroupId,
|
request.AssignedGroupCode,
|
||||||
request.AssignedGroupId,
|
|
||||||
request.Confidential,
|
request.Confidential,
|
||||||
request.FreeTitle,
|
request.FreeTitle);
|
||||||
request.SiaCode);
|
|
||||||
|
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 });
|
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")]
|
[HttpPost("thirds/ensure-link")]
|
||||||
public async Task<IActionResult> EnsureThirdAndLink(
|
public async Task<IActionResult> EnsureThirdAndLink(
|
||||||
GestionaEnsureThirdRequest request,
|
GestionaEnsureThirdRequest request,
|
||||||
@@ -98,8 +134,7 @@ public sealed class GestionaController : ControllerBase
|
|||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
await _gestiona.AsegurarTerceroYEnlazarAsync(request.FileUrl, request.ThirdParty);
|
return Ok(await _gestiona.AsegurarTerceroYEnlazarAsync(request.FileUrl, request.ThirdParty));
|
||||||
return Ok(new { ok = true });
|
|
||||||
}
|
}
|
||||||
catch (ArgumentException ex)
|
catch (ArgumentException ex)
|
||||||
{
|
{
|
||||||
@@ -157,8 +192,9 @@ public sealed class GestionaController : ControllerBase
|
|||||||
{
|
{
|
||||||
await _workflow.TramitarDocumentoAsync(
|
await _workflow.TramitarDocumentoAsync(
|
||||||
request.DocumentUrl,
|
request.DocumentUrl,
|
||||||
request.AssignedGroupHref,
|
request.AssignedGroupCode,
|
||||||
request.ComplaintId);
|
request.ComplaintId,
|
||||||
|
request.IsUpdate);
|
||||||
|
|
||||||
return Ok(new { ok = true });
|
return Ok(new { ok = true });
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,6 +10,13 @@ namespace ApiDenuncias.Controllers;
|
|||||||
[Route("api/inbox")]
|
[Route("api/inbox")]
|
||||||
public sealed class InboxController : ControllerBase
|
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 GlobalLeaksSessionStore _sessionStore;
|
||||||
private readonly PendingGlobalLeaksLoginStore _pendingLoginStore;
|
private readonly PendingGlobalLeaksLoginStore _pendingLoginStore;
|
||||||
private readonly GlobalLeaksClient _globalLeaksClient;
|
private readonly GlobalLeaksClient _globalLeaksClient;
|
||||||
@@ -74,7 +81,7 @@ public sealed class InboxController : ControllerBase
|
|||||||
_logger.LogError(ex, "No se ha podido preparar la renovacion GlobalLeaks para {Username}.", username);
|
_logger.LogError(ex, "No se ha podido preparar la renovacion GlobalLeaks para {Username}.", username);
|
||||||
return StatusCode(
|
return StatusCode(
|
||||||
StatusCodes.Status500InternalServerError,
|
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."));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -141,7 +148,7 @@ public sealed class InboxController : ControllerBase
|
|||||||
_logger.LogError(ex, "No se ha podido cargar la bandeja GlobalLeaks para {Username}.", username);
|
_logger.LogError(ex, "No se ha podido cargar la bandeja GlobalLeaks para {Username}.", username);
|
||||||
return StatusCode(
|
return StatusCode(
|
||||||
StatusCodes.Status500InternalServerError,
|
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."));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -164,12 +171,17 @@ public sealed class InboxController : ControllerBase
|
|||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
var state = await _trackingService.GetUserStateAsync(username, cancellationToken);
|
||||||
var contexts = await _globalLeaksClient.GetContextsAsync(session.SessionId!, cancellationToken);
|
var contexts = await _globalLeaksClient.GetContextsAsync(session.SessionId!, cancellationToken);
|
||||||
var reports = await _globalLeaksClient.GetReportsAsync(session.SessionId!, "all", null, null, cancellationToken, contexts);
|
var reports = await _globalLeaksClient.GetReportsAsync(session.SessionId!, "all", null, null, cancellationToken, contexts);
|
||||||
var enrichedReports = await _trackingService.RegisterSnapshotAsync(username, reports, cancellationToken);
|
var enrichedReports = await _trackingService.RegisterSnapshotAsync(username, reports, cancellationToken);
|
||||||
var state = await _trackingService.GetUserStateAsync(username, cancellationToken);
|
var activityReports = await _globalLeaksClient.EnrichReportsWithActivityAsync(
|
||||||
|
session.SessionId!,
|
||||||
|
enrichedReports,
|
||||||
|
state.LastDownloadedReportMomentUtc,
|
||||||
|
cancellationToken);
|
||||||
|
|
||||||
return Ok(new InboxSnapshotResponse(contexts, enrichedReports, state));
|
return Ok(new InboxSnapshotResponse(contexts, activityReports, state));
|
||||||
}
|
}
|
||||||
catch (GlobalLeaksSessionExpiredException)
|
catch (GlobalLeaksSessionExpiredException)
|
||||||
{
|
{
|
||||||
@@ -178,14 +190,14 @@ public sealed class InboxController : ControllerBase
|
|||||||
}
|
}
|
||||||
catch (GlobalLeaksValidationException ex)
|
catch (GlobalLeaksValidationException ex)
|
||||||
{
|
{
|
||||||
return StatusCode(ex.StatusCode, new ApiError(ex.Message));
|
return ToGlobalLeaksApiError(ex, "cargar la bandeja de GlobalLeaks");
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
_logger.LogError(ex, "No se ha podido cargar la bandeja GlobalLeaks para {Username}.", username);
|
_logger.LogError(ex, "No se ha podido cargar la bandeja GlobalLeaks para {Username}.", username);
|
||||||
return StatusCode(
|
return StatusCode(
|
||||||
StatusCodes.Status500InternalServerError,
|
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."));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -210,7 +222,25 @@ public sealed class InboxController : ControllerBase
|
|||||||
{
|
{
|
||||||
await _trackingService.EnsureReportCanBeImportedByUserAsync(username, report, cancellationToken);
|
await _trackingService.EnsureReportCanBeImportedByUserAsync(username, report, 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);
|
||||||
|
}
|
||||||
|
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.SessionId!, report, cancellationToken);
|
||||||
|
|
||||||
FileDownloadResult? json = null;
|
FileDownloadResult? json = null;
|
||||||
try
|
try
|
||||||
@@ -222,7 +252,7 @@ public sealed class InboxController : ControllerBase
|
|||||||
json = null;
|
json = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
var result = await _inboxService.ImportFromGlobalLeaksAsync(zip, json, cancellationToken);
|
var result = await _inboxService.ImportFromGlobalLeaksAsync(reportPackage, json, reportDetail, cancellationToken);
|
||||||
if (result.ImportedCount > 0)
|
if (result.ImportedCount > 0)
|
||||||
{
|
{
|
||||||
await _trackingService.MarkReportImportedAsync(
|
await _trackingService.MarkReportImportedAsync(
|
||||||
@@ -241,14 +271,14 @@ public sealed class InboxController : ControllerBase
|
|||||||
}
|
}
|
||||||
catch (GlobalLeaksValidationException ex)
|
catch (GlobalLeaksValidationException ex)
|
||||||
{
|
{
|
||||||
return StatusCode(ex.StatusCode, new ApiError(ex.Message));
|
return ToGlobalLeaksApiError(ex, "importar la denuncia");
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
_logger.LogError(ex, "No se ha podido importar la denuncia {ReportId} para {Username}.", reportId, username);
|
_logger.LogError(ex, "No se ha podido importar la denuncia {ReportId} para {Username}.", reportId, username);
|
||||||
return StatusCode(
|
return StatusCode(
|
||||||
StatusCodes.Status500InternalServerError,
|
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."));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -276,37 +306,78 @@ public sealed class InboxController : ControllerBase
|
|||||||
}
|
}
|
||||||
catch (GlobalLeaksValidationException ex)
|
catch (GlobalLeaksValidationException ex)
|
||||||
{
|
{
|
||||||
return StatusCode(ex.StatusCode, new ApiError(ex.Message));
|
return ToGlobalLeaksApiError(ex, "leer el detalle de la denuncia");
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
_logger.LogError(ex, "No se ha podido leer el detalle de la denuncia {ReportId} para {Username}.", reportId, username);
|
_logger.LogError(ex, "No se ha podido leer el detalle de la denuncia {ReportId} para {Username}.", reportId, username);
|
||||||
return StatusCode(
|
return StatusCode(
|
||||||
StatusCodes.Status500InternalServerError,
|
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")]
|
private async Task<FileDownloadResult> DownloadReportPackageWithRetryAsync(
|
||||||
public async Task<IActionResult> EnsureStorage(CancellationToken cancellationToken)
|
string sessionId,
|
||||||
|
ReportDto report,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
await _inboxService.EnsureStorageReadyAsync(cancellationToken);
|
for (var attempt = 0; ; attempt++)
|
||||||
return Ok(new { ok = true });
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return await _globalLeaksClient.DownloadReportPackageAsync(sessionId, report.Id, cancellationToken);
|
||||||
|
}
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpPost("local/process")]
|
private static bool IsExportNotReady(GlobalLeaksValidationException ex)
|
||||||
public async Task<ActionResult<ImportSummary>> ProcessLocalZips(CancellationToken cancellationToken)
|
=> ex.StatusCode is >= 500 and <= 504;
|
||||||
=> Ok(await _inboxService.ProcessPendingFolderZipsAsync(cancellationToken));
|
|
||||||
|
|
||||||
[HttpGet("local/zips")]
|
private static ActionResult ToGlobalLeaksApiError(GlobalLeaksValidationException ex, string operation)
|
||||||
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)
|
|
||||||
{
|
{
|
||||||
await _inboxService.DeleteZipAsync(zipName, cancellationToken);
|
if (ex.StatusCode == StatusCodes.Status401Unauthorized ||
|
||||||
return Ok(new { ok = true });
|
ex.StatusCode == StatusCodes.Status403Forbidden)
|
||||||
|
{
|
||||||
|
return new ObjectResult(new ApiError(
|
||||||
|
"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."))
|
||||||
|
{
|
||||||
|
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 async Task<GlobalLeaksStoredSession?> RequireActiveSessionAsync(string username, CancellationToken cancellationToken)
|
private async Task<GlobalLeaksStoredSession?> RequireActiveSessionAsync(string username, CancellationToken cancellationToken)
|
||||||
@@ -322,4 +393,7 @@ public sealed class InboxController : ControllerBase
|
|||||||
=> session is null
|
=> session is null
|
||||||
? null
|
? null
|
||||||
: new ApiGlobalLeaksSessionDto(session.Username, session.Role, session.HasActiveSession, session.UpdatedAt);
|
: new ApiGlobalLeaksSessionDto(session.Username, session.Role, session.HasActiveSession, session.UpdatedAt);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ using Microsoft.AspNetCore.Mvc;
|
|||||||
namespace ApiDenuncias.Controllers;
|
namespace ApiDenuncias.Controllers;
|
||||||
|
|
||||||
[ApiController]
|
[ApiController]
|
||||||
[Authorize]
|
[Authorize(Policy = "ConfigurationAdministrators")]
|
||||||
[Route("api/purge")]
|
[Route("api/purge")]
|
||||||
public sealed class PurgeController : ControllerBase
|
public sealed class PurgeController : ControllerBase
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -34,6 +34,19 @@ public sealed class TrackingController : ControllerBase
|
|||||||
return Ok(new { ok = true });
|
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")]
|
[HttpPost("import-permission")]
|
||||||
public async Task<IActionResult> EnsureImportPermission(
|
public async Task<IActionResult> EnsureImportPermission(
|
||||||
TrackingImportPermissionRequest request,
|
TrackingImportPermissionRequest request,
|
||||||
|
|||||||
@@ -42,6 +42,7 @@ builder.Services.AddScoped<IFilteredDenunciaStore>(sp => sp.GetRequiredService<E
|
|||||||
builder.Services.AddScoped<IInboxTrackingService, InboxTrackingService>();
|
builder.Services.AddScoped<IInboxTrackingService, InboxTrackingService>();
|
||||||
builder.Services.AddScoped<DenunciaInboxService>();
|
builder.Services.AddScoped<DenunciaInboxService>();
|
||||||
builder.Services.AddScoped<GestionaDocumentWorkflowService>();
|
builder.Services.AddScoped<GestionaDocumentWorkflowService>();
|
||||||
|
builder.Services.AddScoped<GestionaExpedienteExceptionStore>();
|
||||||
builder.Services.AddScoped<UserComplaintAccessService>();
|
builder.Services.AddScoped<UserComplaintAccessService>();
|
||||||
builder.Services.AddHttpClient<ManualPurgeService>();
|
builder.Services.AddHttpClient<ManualPurgeService>();
|
||||||
builder.Services.AddScoped<AppConfigurationService>();
|
builder.Services.AddScoped<AppConfigurationService>();
|
||||||
@@ -99,7 +100,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();
|
var app = builder.Build();
|
||||||
|
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -64,6 +64,8 @@ CREATE TABLE IF NOT EXISTS complaints (
|
|||||||
workflow_status TEXT NOT NULL,
|
workflow_status TEXT NOT NULL,
|
||||||
selected_document_name TEXT NULL,
|
selected_document_name TEXT NULL,
|
||||||
gestiona_uploaded_at_utc DATETIME(6) NULL,
|
gestiona_uploaded_at_utc DATETIME(6) NULL,
|
||||||
|
gestiona_last_upload_type TEXT NOT NULL,
|
||||||
|
gestiona_assigned_group TEXT NOT NULL,
|
||||||
is_in_gestiona TINYINT(1) NOT NULL DEFAULT 0,
|
is_in_gestiona TINYINT(1) NOT NULL DEFAULT 0,
|
||||||
is_rejected 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),
|
created_at_utc DATETIME(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6),
|
||||||
@@ -133,6 +135,24 @@ CREATE TABLE IF NOT EXISTS user_inbox_reports (
|
|||||||
ON DELETE CASCADE
|
ON DELETE CASCADE
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
|
) 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 (
|
CREATE TABLE IF NOT EXISTS complaint_attachments (
|
||||||
id BIGINT NOT NULL AUTO_INCREMENT,
|
id BIGINT NOT NULL AUTO_INCREMENT,
|
||||||
complaint_id BIGINT NOT NULL,
|
complaint_id BIGINT NOT NULL,
|
||||||
|
|||||||
@@ -35,7 +35,12 @@ public sealed class AppConfigurationService
|
|||||||
? null
|
? null
|
||||||
: Convert.ToString(value, CultureInfo.InvariantCulture);
|
: 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(
|
public async Task<AppConfigurationDto> SetExternalUpdateCutoffDateAsync(
|
||||||
@@ -59,7 +64,42 @@ public sealed class AppConfigurationService
|
|||||||
command.Parameters.AddWithValue("@settingValue", string.IsNullOrWhiteSpace(dateText) ? DBNull.Value : dateText);
|
command.Parameters.AddWithValue("@settingValue", string.IsNullOrWhiteSpace(dateText) ? DBNull.Value : dateText);
|
||||||
|
|
||||||
await command.ExecuteNonQueryAsync(cancellationToken);
|
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)
|
private async Task<MySqlConnection> OpenConnectionAsync(CancellationToken cancellationToken)
|
||||||
@@ -84,4 +124,6 @@ public sealed class AppConfigurationService
|
|||||||
connection);
|
connection);
|
||||||
await command.ExecuteNonQueryAsync(cancellationToken);
|
await command.ExecuteNonQueryAsync(cancellationToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private sealed record LatestEncryptionKey(DateOnly KeyDate, string Status);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
using System.IO.Compression;
|
using System.IO.Compression;
|
||||||
|
using System.Globalization;
|
||||||
|
using System.Security.Cryptography;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using ApiDenuncias.Helpers;
|
using ApiDenuncias.Helpers;
|
||||||
using GestionaDenuncias.Shared.Models;
|
using GestionaDenuncias.Shared.Models;
|
||||||
@@ -7,8 +9,6 @@ namespace ApiDenuncias.Services;
|
|||||||
|
|
||||||
public sealed class DenunciaInboxService
|
public sealed class DenunciaInboxService
|
||||||
{
|
{
|
||||||
private const string RootPath = @"C:\ZipsDenuncias";
|
|
||||||
|
|
||||||
private static readonly HashSet<string> BlockedAttachmentExtensions = new(StringComparer.OrdinalIgnoreCase)
|
private static readonly HashSet<string> BlockedAttachmentExtensions = new(StringComparer.OrdinalIgnoreCase)
|
||||||
{
|
{
|
||||||
".ade",
|
".ade",
|
||||||
@@ -55,91 +55,46 @@ public sealed class DenunciaInboxService
|
|||||||
};
|
};
|
||||||
|
|
||||||
private readonly IGestionaService _gestionaService;
|
private readonly IGestionaService _gestionaService;
|
||||||
|
private readonly GestionaExpedienteExceptionStore _gestionaExceptionStore;
|
||||||
private readonly IDenunciaStore _denunciaStore;
|
private readonly IDenunciaStore _denunciaStore;
|
||||||
|
private readonly IFilteredDenunciaStore _filteredDenunciaStore;
|
||||||
private readonly ILogger<DenunciaInboxService> _logger;
|
private readonly ILogger<DenunciaInboxService> _logger;
|
||||||
|
|
||||||
public DenunciaInboxService(
|
public DenunciaInboxService(
|
||||||
IGestionaService gestionaService,
|
IGestionaService gestionaService,
|
||||||
|
GestionaExpedienteExceptionStore gestionaExceptionStore,
|
||||||
IDenunciaStore denunciaStore,
|
IDenunciaStore denunciaStore,
|
||||||
|
IFilteredDenunciaStore filteredDenunciaStore,
|
||||||
ILogger<DenunciaInboxService> logger)
|
ILogger<DenunciaInboxService> logger)
|
||||||
{
|
{
|
||||||
_gestionaService = gestionaService;
|
_gestionaService = gestionaService;
|
||||||
|
_gestionaExceptionStore = gestionaExceptionStore;
|
||||||
_denunciaStore = denunciaStore;
|
_denunciaStore = denunciaStore;
|
||||||
|
_filteredDenunciaStore = filteredDenunciaStore;
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task EnsureStorageReadyAsync(CancellationToken cancellationToken = default)
|
public async Task EnsureStorageReadyAsync(CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
Directory.CreateDirectory(RootPath);
|
|
||||||
await _denunciaStore.EnsureSchemaAsync(cancellationToken);
|
await _denunciaStore.EnsureSchemaAsync(cancellationToken);
|
||||||
}
|
await _gestionaExceptionStore.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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<ImportSummary> ImportFromGlobalLeaksAsync(
|
public async Task<ImportSummary> ImportFromGlobalLeaksAsync(
|
||||||
FileDownloadResult zipDownload,
|
FileDownloadResult reportDownload,
|
||||||
FileDownloadResult? jsonDownload,
|
FileDownloadResult? jsonDownload,
|
||||||
|
ReportDetailDto? reportDetail,
|
||||||
CancellationToken cancellationToken = default)
|
CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
await EnsureStorageReadyAsync(cancellationToken);
|
await EnsureStorageReadyAsync(cancellationToken);
|
||||||
|
|
||||||
var fileName = string.IsNullOrWhiteSpace(zipDownload.FileName)
|
var sourceName = string.IsNullOrWhiteSpace(reportDownload.FileName)
|
||||||
? $"report-{Guid.NewGuid():N}.zip"
|
? $"report-{Guid.NewGuid():N}"
|
||||||
: zipDownload.FileName;
|
: reportDownload.FileName;
|
||||||
|
if (sourceName.EndsWith(".zip", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
sourceName = Path.GetFileNameWithoutExtension(sourceName);
|
||||||
|
}
|
||||||
|
|
||||||
var json = jsonDownload is null
|
var json = jsonDownload is null
|
||||||
? null
|
? null
|
||||||
@@ -147,24 +102,72 @@ public sealed class DenunciaInboxService
|
|||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var result = await ProcessZipAsync(zipDownload.Content, fileName, json, cancellationToken);
|
var result = await ProcessGlobalLeaksPackageAsync(reportDownload.Content, sourceName, json, reportDetail, cancellationToken);
|
||||||
return new ImportSummary(1, 1, [], [result.ComplaintId], result.Warnings);
|
return new ImportSummary(
|
||||||
|
1,
|
||||||
|
result.ImportedCount,
|
||||||
|
[],
|
||||||
|
result.ImportedCount > 0 ? [result.ComplaintId] : [],
|
||||||
|
result.Warnings);
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
_logger.LogError(ex, "Error importando denuncia desde GlobalLeaks {FileName}", fileName);
|
_logger.LogError(ex, "Error importando denuncia desde GlobalLeaks {SourceName}", sourceName);
|
||||||
return new ImportSummary(1, 0, [$"{fileName}: {ex.Message}"]);
|
return new ImportSummary(1, 0, [ToUserImportErrorMessage(sourceName, ex)]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<ProcessZipResult> ProcessZipAsync(
|
private static string ToUserImportErrorMessage(string sourceName, Exception ex)
|
||||||
byte[] zipBytes,
|
{
|
||||||
|
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 sourceName,
|
||||||
string? globalLeaksJson,
|
string? globalLeaksJson,
|
||||||
|
ReportDetailDto? reportDetail,
|
||||||
CancellationToken cancellationToken)
|
CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
using var zipStream = new MemoryStream(zipBytes, writable: false);
|
using var packageStream = new MemoryStream(packageBytes, writable: false);
|
||||||
using var archive = new ZipArchive(zipStream, ZipArchiveMode.Read, leaveOpen: false);
|
using var archive = new ZipArchive(packageStream, ZipArchiveMode.Read, leaveOpen: false);
|
||||||
|
|
||||||
var reportEntry = FindReportEntry(archive);
|
var reportEntry = FindReportEntry(archive);
|
||||||
|
|
||||||
@@ -178,8 +181,8 @@ public sealed class DenunciaInboxService
|
|||||||
|
|
||||||
throw new InvalidOperationException(
|
throw new InvalidOperationException(
|
||||||
entries.Length == 0
|
entries.Length == 0
|
||||||
? "El ZIP no contiene ficheros."
|
? "El paquete de GlobalLeaks no contiene ficheros."
|
||||||
: $"El ZIP no contiene un report reconocible. Ficheros encontrados: {string.Join(", ", entries)}");
|
: $"El paquete de GlobalLeaks no contiene un report reconocible. Ficheros encontrados: {string.Join(", ", entries)}");
|
||||||
}
|
}
|
||||||
|
|
||||||
var reportIsPdf = IsPdfEntry(reportEntry);
|
var reportIsPdf = IsPdfEntry(reportEntry);
|
||||||
@@ -224,16 +227,51 @@ public sealed class DenunciaInboxService
|
|||||||
denuncia.Expediente_Gestiona = "Pendiente";
|
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);
|
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);
|
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;
|
||||||
|
await _denunciaStore.UpsertDenunciaAsync(storedComplaint, cancellationToken);
|
||||||
|
|
||||||
|
warnings.Add($"La denuncia #{denuncia.Id_Denuncia} ya esta en Gestiona y no tiene documentos nuevos pendientes de subir.");
|
||||||
|
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(
|
private async Task<ReadFilesResult> ReadFilesFromArchiveAsync(
|
||||||
ZipArchive archive,
|
ZipArchive archive,
|
||||||
ZipArchiveEntry reportEntry,
|
ZipArchiveEntry reportEntry,
|
||||||
int denunciaId,
|
int denunciaId,
|
||||||
|
DateTime reportDateUtc,
|
||||||
|
ReportDetailDto? reportDetail,
|
||||||
CancellationToken cancellationToken)
|
CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
var warnings = new List<string>();
|
var warnings = new List<string>();
|
||||||
@@ -243,9 +281,9 @@ public sealed class DenunciaInboxService
|
|||||||
id_Fichero: 0,
|
id_Fichero: 0,
|
||||||
id_Tipo: 1,
|
id_Tipo: 1,
|
||||||
descripcion: IsPdfEntry(reportEntry) ? "report.pdf original" : "report.txt original",
|
descripcion: IsPdfEntry(reportEntry) ? "report.pdf original" : "report.txt original",
|
||||||
fecha: reportEntry.LastWriteTime.UtcDateTime == DateTime.MinValue
|
fecha: reportDateUtc == DateTime.MinValue
|
||||||
? DateTime.UtcNow
|
? GetEntryFallbackDateUtc(reportEntry)
|
||||||
: reportEntry.LastWriteTime.UtcDateTime,
|
: NormalizeUtc(reportDateUtc),
|
||||||
observaciones: "",
|
observaciones: "",
|
||||||
id_Denuncia: denunciaId,
|
id_Denuncia: denunciaId,
|
||||||
nombreFichero: IsPdfEntry(reportEntry) ? "report.pdf" : "report.txt",
|
nombreFichero: IsPdfEntry(reportEntry) ? "report.pdf" : "report.txt",
|
||||||
@@ -264,9 +302,7 @@ public sealed class DenunciaInboxService
|
|||||||
id_Fichero: 0,
|
id_Fichero: 0,
|
||||||
id_Tipo: 1,
|
id_Tipo: 1,
|
||||||
descripcion: null,
|
descripcion: null,
|
||||||
fecha: entry.LastWriteTime.UtcDateTime == DateTime.MinValue
|
fecha: ResolveAttachmentDateUtc(entry, reportDetail),
|
||||||
? DateTime.UtcNow
|
|
||||||
: entry.LastWriteTime.UtcDateTime,
|
|
||||||
observaciones: "",
|
observaciones: "",
|
||||||
id_Denuncia: denunciaId,
|
id_Denuncia: denunciaId,
|
||||||
nombreFichero: Path.GetFileName(entry.FullName),
|
nombreFichero: Path.GetFileName(entry.FullName),
|
||||||
@@ -308,6 +344,67 @@ public sealed class DenunciaInboxService
|
|||||||
return _denunciaStore.UpsertFicherosAsync(nuevosFicheros, cancellationToken);
|
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 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(
|
private async Task CompleteGestionaStatusAsync(
|
||||||
DenunciasGestiona denuncia,
|
DenunciasGestiona denuncia,
|
||||||
CancellationToken cancellationToken)
|
CancellationToken cancellationToken)
|
||||||
@@ -329,16 +426,16 @@ public sealed class DenunciaInboxService
|
|||||||
{
|
{
|
||||||
var match = await _gestionaService.BuscarExpedientePorIdEnAsuntoAsync(denuncia.Id_Denuncia);
|
var match = await _gestionaService.BuscarExpedientePorIdEnAsuntoAsync(denuncia.Id_Denuncia);
|
||||||
if (match is null)
|
if (match is null)
|
||||||
|
{
|
||||||
|
match = await TryFindGestionaExceptionMatchAsync(denuncia, cancellationToken);
|
||||||
|
if (match is null)
|
||||||
{
|
{
|
||||||
ApplyPendingGestionaStatus(denuncia);
|
ApplyPendingGestionaStatus(denuncia);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
denuncia.EsActualizacion = true;
|
ApplyGestionaMatch(denuncia, match);
|
||||||
denuncia.EnGestiona = true;
|
|
||||||
denuncia.Expediente_Gestiona = match.FileUrl;
|
|
||||||
denuncia.CodigoExpedienteGestiona = match.CodigoExpediente ?? string.Empty;
|
|
||||||
denuncia.NombreDenuncia = match.FreeTitle ?? $"Denuncia {denuncia.Id_Denuncia}-CD";
|
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
@@ -350,6 +447,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(
|
private static bool TryApplyGestionaStatusFromStore(
|
||||||
DenunciasGestiona target,
|
DenunciasGestiona target,
|
||||||
DenunciasGestiona? storedComplaint)
|
DenunciasGestiona? storedComplaint)
|
||||||
@@ -474,6 +604,83 @@ public sealed class DenunciaInboxService
|
|||||||
return IsDirectChildOf(normalized, "files") || IsDirectChildOf(normalized, "files_attached_from_recipients");
|
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)
|
private static bool IsBlockedAttachmentEntry(ZipArchiveEntry entry)
|
||||||
{
|
{
|
||||||
var extension = Path.GetExtension(entry.Name);
|
var extension = Path.GetExtension(entry.Name);
|
||||||
@@ -667,7 +874,7 @@ public sealed class DenunciaInboxService
|
|||||||
builder.AppendLine();
|
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);
|
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);
|
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(
|
private async Task<List<FicherosDenuncias>> UnprotectAttachmentsAsync(
|
||||||
List<FicherosDenuncias> ficheros,
|
List<FicherosDenuncias> ficheros,
|
||||||
bool skipPurgedRows,
|
bool skipPurgedRows,
|
||||||
@@ -165,6 +210,12 @@ public sealed class EncryptedDenunciaStore : IDenunciaStore, IFilteredDenunciaSt
|
|||||||
await _inner.UpsertFicherosAsync(ficheros.Select(fichero => ProtectAttachment(fichero, key)).ToArray(), cancellationToken);
|
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(
|
public Task MarkFicherosAsUploadedAsync(
|
||||||
int denunciaId,
|
int denunciaId,
|
||||||
IEnumerable<string> fileNames,
|
IEnumerable<string> fileNames,
|
||||||
@@ -219,6 +270,8 @@ public sealed class EncryptedDenunciaStore : IDenunciaStore, IFilteredDenunciaSt
|
|||||||
EstadoDenuncia = source.EstadoDenuncia,
|
EstadoDenuncia = source.EstadoDenuncia,
|
||||||
ArchivoElegido = source.ArchivoElegido,
|
ArchivoElegido = source.ArchivoElegido,
|
||||||
FechaSubidaAGestiona = source.FechaSubidaAGestiona,
|
FechaSubidaAGestiona = source.FechaSubidaAGestiona,
|
||||||
|
UltimaSubidaGestionaTipo = source.UltimaSubidaGestionaTipo,
|
||||||
|
UltimoGrupoAsignadoGestiona = source.UltimoGrupoAsignadoGestiona,
|
||||||
EnGestiona = source.EnGestiona,
|
EnGestiona = source.EnGestiona,
|
||||||
EnRechazada = source.EnRechazada,
|
EnRechazada = source.EnRechazada,
|
||||||
|
|
||||||
@@ -252,6 +305,18 @@ public sealed class EncryptedDenunciaStore : IDenunciaStore, IFilteredDenunciaSt
|
|||||||
return rebuilt;
|
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)
|
private static DenunciasGestiona? TryParseStoredReport(DenunciasGestiona stored)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrWhiteSpace(stored.TextoOriginalReport))
|
if (string.IsNullOrWhiteSpace(stored.TextoOriginalReport))
|
||||||
@@ -286,6 +351,8 @@ public sealed class EncryptedDenunciaStore : IDenunciaStore, IFilteredDenunciaSt
|
|||||||
target.EstadoDenuncia = stored.EstadoDenuncia;
|
target.EstadoDenuncia = stored.EstadoDenuncia;
|
||||||
target.ArchivoElegido = stored.ArchivoElegido;
|
target.ArchivoElegido = stored.ArchivoElegido;
|
||||||
target.FechaSubidaAGestiona = stored.FechaSubidaAGestiona;
|
target.FechaSubidaAGestiona = stored.FechaSubidaAGestiona;
|
||||||
|
target.UltimaSubidaGestionaTipo = stored.UltimaSubidaGestionaTipo;
|
||||||
|
target.UltimoGrupoAsignadoGestiona = stored.UltimoGrupoAsignadoGestiona;
|
||||||
target.EnGestiona = stored.EnGestiona;
|
target.EnGestiona = stored.EnGestiona;
|
||||||
target.EnRechazada = stored.EnRechazada;
|
target.EnRechazada = stored.EnRechazada;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
using System.Net.Http.Headers;
|
using System.Net.Http.Headers;
|
||||||
using System.Security.Cryptography;
|
using System.Security.Cryptography;
|
||||||
|
using System.Globalization;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
@@ -35,6 +36,17 @@ public sealed class GestionaDocumentWorkflowService
|
|||||||
_configuration["Gestiona:AccessToken"]
|
_configuration["Gestiona:AccessToken"]
|
||||||
?? throw new InvalidOperationException("Falta Gestiona:AccessToken en appsettings.");
|
?? 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)
|
public async Task<string> UploadDocumentAndReturnUrlAsync(string fileUrl, byte[] contentBytes, string fileName)
|
||||||
{
|
{
|
||||||
var fileUrlAbs = EnsureAbsoluteGestionaUrl(fileUrl, GestionaApiBase);
|
var fileUrlAbs = EnsureAbsoluteGestionaUrl(fileUrl, GestionaApiBase);
|
||||||
@@ -47,7 +59,9 @@ public sealed class GestionaDocumentWorkflowService
|
|||||||
name = fileName,
|
name = fileName,
|
||||||
description = "Documento de denuncia",
|
description = "Documento de denuncia",
|
||||||
elaboration_state = "EE01",
|
elaboration_state = "EE01",
|
||||||
metadata_language = "ES",
|
metadata_language = DocumentMetadataLanguage,
|
||||||
|
metadata_type = DocumentMetadataType,
|
||||||
|
metadata_subtype = DocumentMetadataSubtype,
|
||||||
links = new[] { new { rel = "content", href = uploadUri } }
|
links = new[] { new { rel = "content", href = uploadUri } }
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -106,28 +120,24 @@ public sealed class GestionaDocumentWorkflowService
|
|||||||
throw new InvalidOperationException("No se pudo obtener la URL del documento creado en Gestiona.");
|
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)
|
||||||
{
|
{
|
||||||
_ = assignedGroupHref;
|
|
||||||
|
|
||||||
var docUrlAbs = EnsureAbsoluteGestionaUrl(documentUrl, GestionaApiBase);
|
var docUrlAbs = EnsureAbsoluteGestionaUrl(documentUrl, GestionaApiBase);
|
||||||
var templateHref = GetConfiguredTemplateHref(docUrlAbs);
|
var operationLabel = isUpdate ? $"actualizacion grupo {NormalizeGroupCode(assignedGroupCode)}" : "nueva denuncia";
|
||||||
|
var (templateHref, payload) = await ResolveCircuitTemplatePayloadAsync(docUrlAbs, isUpdate, assignedGroupCode);
|
||||||
if (string.IsNullOrWhiteSpace(templateHref))
|
|
||||||
{
|
|
||||||
throw new InvalidOperationException(
|
|
||||||
"Falta Gestiona:CircuitTemplateId. No se listan plantillas para evitar campos deprecated.");
|
|
||||||
}
|
|
||||||
|
|
||||||
var payload = await GetCircuitTemplatePayloadAsync(templateHref);
|
|
||||||
var (success, statusCode, body) = await TryPostCircuitAsync(docUrlAbs, payload);
|
var (success, statusCode, body) = await TryPostCircuitAsync(docUrlAbs, payload);
|
||||||
if (success)
|
if (success)
|
||||||
{
|
{
|
||||||
_logger.LogInformation(
|
_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,
|
docUrlAbs,
|
||||||
templateHref,
|
templateHref,
|
||||||
complaintId);
|
complaintId,
|
||||||
|
operationLabel);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -142,6 +152,171 @@ public sealed class GestionaDocumentWorkflowService
|
|||||||
$"TramitarDocumentoAsync: {(int)statusCode} {statusCode}\n{body}");
|
$"TramitarDocumentoAsync: {(int)statusCode} {statusCode}\n{body}");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async Task<(string TemplateHref, string Payload)> ResolveCircuitTemplatePayloadAsync(
|
||||||
|
string documentUrl,
|
||||||
|
bool isUpdate,
|
||||||
|
string? assignedGroupCode)
|
||||||
|
{
|
||||||
|
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);
|
||||||
|
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)
|
||||||
|
{
|
||||||
|
if (!isUpdate)
|
||||||
|
{
|
||||||
|
return new CircuitTemplateSelection(
|
||||||
|
_configuration["Gestiona:CircuitNewComplaintTemplateName"],
|
||||||
|
Required: true,
|
||||||
|
OperationLabel: "nueva denuncia");
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
private async Task<string> GetCircuitTemplatePayloadAsync(string templateHref)
|
||||||
{
|
{
|
||||||
using var req = new HttpRequestMessage(HttpMethod.Get, templateHref);
|
using var req = new HttpRequestMessage(HttpMethod.Get, templateHref);
|
||||||
@@ -182,14 +357,6 @@ public sealed class GestionaDocumentWorkflowService
|
|||||||
return (resp.IsSuccessStatusCode, resp.StatusCode, body);
|
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 HttpClient CreateRawHttp() => _httpClientFactory.CreateClient();
|
||||||
|
|
||||||
private async Task<string> CreateUploadAsync(byte[] contentBytes, string fileName)
|
private async Task<string> CreateUploadAsync(byte[] contentBytes, string fileName)
|
||||||
@@ -279,6 +446,65 @@ public sealed class GestionaDocumentWorkflowService
|
|||||||
return BitConverter.ToString(hash).Replace("-", "").ToLowerInvariant();
|
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)
|
private void LogDeprecatedHeaders(HttpResponseMessage response, string operation)
|
||||||
{
|
{
|
||||||
if (response.Headers.TryGetValues("X-Gestiona-Deprecated", out var deprecated))
|
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;
|
||||||
using System.Text.Json.Serialization;
|
using System.Text.Json.Serialization;
|
||||||
using System.Text.RegularExpressions;
|
using System.Text.RegularExpressions;
|
||||||
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
namespace ApiDenuncias.Services
|
namespace ApiDenuncias.Services
|
||||||
@@ -106,17 +107,12 @@ namespace ApiDenuncias.Services
|
|||||||
// Expedientes (file)
|
// 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;
|
_ = subject;
|
||||||
_ = documentSeries;
|
_ = documentSeries;
|
||||||
_ = siaCode;
|
|
||||||
|
|
||||||
var effectiveProcedureId = procedureId == Guid.Empty
|
var url = await ResolveExternalProcedureCreateFileUrlAsync(siaCode);
|
||||||
? Guid.Parse("82722c9b-cecc-4299-8a7b-ce5abeb8170b")
|
|
||||||
: procedureId;
|
|
||||||
|
|
||||||
var url = await ResolveExternalProcedureCreateFileUrlAsync(effectiveProcedureId);
|
|
||||||
using var req = new HttpRequestMessage(HttpMethod.Post, url);
|
using var req = new HttpRequestMessage(HttpMethod.Post, url);
|
||||||
req.Headers.Accept.Clear();
|
req.Headers.Accept.Clear();
|
||||||
req.Headers.Accept.Add(
|
req.Headers.Accept.Add(
|
||||||
@@ -138,24 +134,263 @@ namespace ApiDenuncias.Services
|
|||||||
return new GestionaCreateFileResponse(fileUrl, fileOpenUrl);
|
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)
|
var proceduresUrl = await ResolveRootLinkHrefAsync("vnd.gestiona.catalog-2015")
|
||||||
? configuredExternalProcedureId
|
?? "/rest/catalog-2015/procedures";
|
||||||
: procedureId;
|
var procedures = await GetContentArrayAsync(
|
||||||
|
proceduresUrl,
|
||||||
|
"application/vnd.gestiona.procedures-2015-page+json",
|
||||||
|
"GET catalogo de procedimientos Gestiona");
|
||||||
|
|
||||||
return Task.FromResult(
|
var procedure = SelectProcedure(procedures, siaCode);
|
||||||
$"/rest/catalog-2015/procedures/{procedureId}/external-procedures/{externalProcedureId}/create-file");
|
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(
|
public async Task OpenFileAsync(
|
||||||
string fileUrl,
|
string fileUrl,
|
||||||
string? fileOpenUrl,
|
string? fileOpenUrl,
|
||||||
Guid managementUnitGroupId,
|
string assignedGroupCode,
|
||||||
Guid assignedGroupId,
|
|
||||||
bool confidential,
|
bool confidential,
|
||||||
string freeTitle,
|
string freeTitle)
|
||||||
string siaCode)
|
|
||||||
{
|
{
|
||||||
if (string.IsNullOrWhiteSpace(fileOpenUrl))
|
if (string.IsNullOrWhiteSpace(fileOpenUrl))
|
||||||
{
|
{
|
||||||
@@ -164,20 +399,24 @@ namespace ApiDenuncias.Services
|
|||||||
}
|
}
|
||||||
|
|
||||||
var url = fileOpenUrl;
|
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
|
var payload = new
|
||||||
{
|
{
|
||||||
free_title = freeTitle,
|
free_title = freeTitle,
|
||||||
location = siaCode,
|
|
||||||
entry_date = DateTimeOffset.UtcNow.ToUnixTimeSeconds().ToString(),
|
entry_date = DateTimeOffset.UtcNow.ToUnixTimeSeconds().ToString(),
|
||||||
confidential,
|
confidential,
|
||||||
initial_assignation = new[]
|
initial_assignation = new[]
|
||||||
{
|
{
|
||||||
new { rel = "group", href = $"{_opts.ApiBase}/rest/groups/{assignedGroupId}" }
|
new { rel = "group", href = assignedGroupHref }
|
||||||
},
|
},
|
||||||
links = new[]
|
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}");
|
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)
|
public async Task<Guid> CreateFolderAsync(string fileUrl, string folderName)
|
||||||
{
|
{
|
||||||
var endpoint = $"{fileUrl}/documents-and-folders";
|
var endpoint = $"{fileUrl}/documents-and-folders";
|
||||||
@@ -336,16 +636,16 @@ namespace ApiDenuncias.Services
|
|||||||
{
|
{
|
||||||
var filtro = new
|
var filtro = new
|
||||||
{
|
{
|
||||||
result = new { max_results = 25 },
|
nif
|
||||||
filter = new { 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("X-Gestiona-Access-Token", _opts.AccessToken);
|
||||||
req.Headers.TryAddWithoutValidation("Accept", "application/vnd.gestiona.thirds-page+json");
|
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);
|
using var resp = await _http.SendAsync(req);
|
||||||
resp.EnsureSuccessStatusCode();
|
resp.EnsureSuccessStatusCode();
|
||||||
@@ -507,14 +807,18 @@ namespace ApiDenuncias.Services
|
|||||||
throw new InvalidOperationException($"Error EnlazarTerceroExistenteAsync: {resp.StatusCode}\n{body}");
|
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)
|
if (thirdParty is null)
|
||||||
throw new ArgumentNullException(nameof(thirdParty));
|
throw new ArgumentNullException(nameof(thirdParty));
|
||||||
|
|
||||||
thirdParty = NormalizeThirdParty(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);
|
var encontrado = await BuscarTerceroPorNifAsync(thirdParty.DocumentId);
|
||||||
|
|
||||||
@@ -525,19 +829,175 @@ namespace ApiDenuncias.Services
|
|||||||
_logger.LogWarning(
|
_logger.LogWarning(
|
||||||
"Se omite la creacion/enlace del tercero en Gestiona para el expediente {FileUrl}: datos identificativos incompletos.",
|
"Se omite la creacion/enlace del tercero en Gestiona para el expediente {FileUrl}: datos identificativos incompletos.",
|
||||||
fileUrl);
|
fileUrl);
|
||||||
return;
|
return new GestionaEnsureThirdResponse(true, warnings);
|
||||||
}
|
}
|
||||||
|
|
||||||
encontrado = await CrearTerceroAsync(thirdParty);
|
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);
|
await TryEnsureThirdAddressAsync(encontrado.SelfHref, thirdParty.Address);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
var yaEnlazados = await ObtenerTercerosEnlazadosAsync(fileUrl);
|
var yaEnlazados = await ObtenerTercerosEnlazadosAsync(fileUrl);
|
||||||
if (!yaEnlazados.Contains(encontrado.SelfHref))
|
if (!yaEnlazados.Contains(encontrado.SelfHref))
|
||||||
await EnlazarTerceroExistenteAsync(fileUrl, 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)
|
private static ThirdPartyIdentityData NormalizeThirdParty(ThirdPartyIdentityData thirdParty)
|
||||||
@@ -563,6 +1023,9 @@ namespace ApiDenuncias.Services
|
|||||||
BusinessName = businessName,
|
BusinessName = businessName,
|
||||||
Email = (thirdParty.Email ?? string.Empty).Trim(),
|
Email = (thirdParty.Email ?? string.Empty).Trim(),
|
||||||
CountryCode = string.IsNullOrWhiteSpace(thirdParty.CountryCode) ? "ESP" : thirdParty.CountryCode.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
|
Address = thirdParty.Address
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -577,6 +1040,9 @@ namespace ApiDenuncias.Services
|
|||||||
BusinessName = string.Empty,
|
BusinessName = string.Empty,
|
||||||
Email = string.Empty,
|
Email = string.Empty,
|
||||||
CountryCode = string.IsNullOrWhiteSpace(thirdParty.CountryCode) ? "ESP" : thirdParty.CountryCode,
|
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
|
Address = null
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -718,6 +1184,51 @@ namespace ApiDenuncias.Services
|
|||||||
return null;
|
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)
|
public async Task<GestionaExpedienteInfo?> ObtenerExpedienteAsync(string fileUrl)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrWhiteSpace(fileUrl))
|
if (string.IsNullOrWhiteSpace(fileUrl))
|
||||||
@@ -797,13 +1308,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(
|
public async Task<List<ExpedienteTerceroDto>> ObtenerExpedientesPorTerceroAsync(
|
||||||
string nif,
|
string nif,
|
||||||
DateTimeOffset? desde = null,
|
DateTimeOffset? desde = null,
|
||||||
DateTimeOffset? hasta = null,
|
DateTimeOffset? hasta = null,
|
||||||
int maxPages = 1,
|
int maxPages = 1,
|
||||||
int maxResults = 30,
|
int maxResults = 30,
|
||||||
int maxParallel = 6 // de momento NO se usa, dejamos la firma por compatibilidad
|
int maxParallel = 6
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrWhiteSpace(nif))
|
if (string.IsNullOrWhiteSpace(nif))
|
||||||
@@ -811,21 +1387,11 @@ namespace ApiDenuncias.Services
|
|||||||
|
|
||||||
nif = nif.Trim().ToUpperInvariant();
|
nif = nif.Trim().ToUpperInvariant();
|
||||||
_ = maxPages;
|
_ = 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 resultados = new List<ExpedienteTerceroDto>();
|
||||||
var json = await GetFilesAsync(new
|
var json = await GetFilesAsync(new
|
||||||
{
|
{
|
||||||
third_rest_link = new
|
dni = nif
|
||||||
{
|
|
||||||
rel = "third",
|
|
||||||
href = tercero.SelfHref
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
using var doc = JsonDocument.Parse(json);
|
using var doc = JsonDocument.Parse(json);
|
||||||
|
|
||||||
@@ -841,20 +1407,15 @@ namespace ApiDenuncias.Services
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
DateTimeOffset? creation = null;
|
var creation = GetJsonDateTimeOffset(item, "creation_date");
|
||||||
if (item.TryGetProperty("creation_date", out var pCreation))
|
var updated = GetFirstJsonDateTimeOffset(
|
||||||
{
|
item,
|
||||||
if (pCreation.ValueKind == JsonValueKind.Number &&
|
"update_date",
|
||||||
pCreation.TryGetInt64(out var ts))
|
"updated_at",
|
||||||
{
|
"modification_date",
|
||||||
creation = DateTimeOffset.FromUnixTimeSeconds(ts);
|
"modified_at",
|
||||||
}
|
"last_update",
|
||||||
else if (pCreation.ValueKind == JsonValueKind.String &&
|
"last_modified");
|
||||||
long.TryParse(pCreation.GetString(), out var tsString))
|
|
||||||
{
|
|
||||||
creation = DateTimeOffset.FromUnixTimeSeconds(tsString);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (desde.HasValue && creation.HasValue && creation.Value < desde.Value)
|
if (desde.HasValue && creation.HasValue && creation.Value < desde.Value)
|
||||||
continue;
|
continue;
|
||||||
@@ -905,14 +1466,67 @@ namespace ApiDenuncias.Services
|
|||||||
FileUrl = fileUrl,
|
FileUrl = fileUrl,
|
||||||
CodigoExpediente = code,
|
CodigoExpediente = code,
|
||||||
Asunto = asunto,
|
Asunto = asunto,
|
||||||
|
Procedimiento = procedureName,
|
||||||
FechaCreacion = creation,
|
FechaCreacion = creation,
|
||||||
|
FechaUltimaModificacion = updated ?? creation,
|
||||||
Estado = state
|
Estado = state
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await EnrichExpedientesWithAuditAsync(resultados, maxParallel);
|
||||||
return resultados;
|
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)
|
private async Task TryEnsureThirdAddressAsync(string thirdSelfHref, ThirdPartyAddressData address)
|
||||||
{
|
{
|
||||||
if (!address.HasAnyValue)
|
if (!address.HasAnyValue)
|
||||||
@@ -1138,6 +1752,58 @@ namespace ApiDenuncias.Services
|
|||||||
: null;
|
: 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)
|
private static string? FirstNonEmpty(params string?[] values)
|
||||||
{
|
{
|
||||||
foreach (var value in values)
|
foreach (var value in values)
|
||||||
@@ -1171,7 +1837,25 @@ namespace ApiDenuncias.Services
|
|||||||
|
|
||||||
private static string BuildNotificationChannel(ThirdPartyIdentityData thirdParty)
|
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";
|
return "TELEMATIC";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ using System.Globalization;
|
|||||||
using System.Net;
|
using System.Net;
|
||||||
using System.Net.Http.Headers;
|
using System.Net.Http.Headers;
|
||||||
using System.Net.Http.Json;
|
using System.Net.Http.Json;
|
||||||
|
using System.Security.Cryptography;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using System.Text.RegularExpressions;
|
using System.Text.RegularExpressions;
|
||||||
@@ -18,6 +19,8 @@ public sealed record PreparedGlobalLeaksCredentials(string Username, string Fina
|
|||||||
public sealed class GlobalLeaksClient
|
public sealed class GlobalLeaksClient
|
||||||
{
|
{
|
||||||
private static readonly JsonSerializerOptions JsonOptions = new(JsonSerializerDefaults.Web);
|
private static readonly JsonSerializerOptions JsonOptions = new(JsonSerializerDefaults.Web);
|
||||||
|
private const int ActivityAnalysisMaxParallelism = 3;
|
||||||
|
private const string AuthenticationPath = "/api/auth/authentication";
|
||||||
private readonly HttpClient _httpClient;
|
private readonly HttpClient _httpClient;
|
||||||
private readonly ILogger<GlobalLeaksClient> _logger;
|
private readonly ILogger<GlobalLeaksClient> _logger;
|
||||||
private readonly GlobalLeaksOptions _options;
|
private readonly GlobalLeaksOptions _options;
|
||||||
@@ -129,11 +132,12 @@ public sealed class GlobalLeaksClient
|
|||||||
authcode?.Length ?? 0);
|
authcode?.Length ?? 0);
|
||||||
|
|
||||||
var currentTokenAnswer = tokenAnswer;
|
var currentTokenAnswer = tokenAnswer;
|
||||||
|
using var dpopKey = ECDsa.Create(ECCurve.NamedCurves.nistP256);
|
||||||
for (var attempt = 0; attempt < 2; attempt++)
|
for (var attempt = 0; attempt < 2; attempt++)
|
||||||
{
|
{
|
||||||
using var authRequest = CreateRequest(
|
using var authRequest = CreateRequest(
|
||||||
HttpMethod.Post,
|
HttpMethod.Post,
|
||||||
$"/api/auth/authentication?token={Uri.EscapeDataString(currentTokenAnswer)}");
|
AuthenticationPath);
|
||||||
authRequest.Content = CreateJsonContent(new
|
authRequest.Content = CreateJsonContent(new
|
||||||
{
|
{
|
||||||
tid = 1,
|
tid = 1,
|
||||||
@@ -142,8 +146,9 @@ public sealed class GlobalLeaksClient
|
|||||||
authcode = authcode?.Trim() ?? string.Empty,
|
authcode = authcode?.Trim() ?? string.Empty,
|
||||||
});
|
});
|
||||||
authRequest.Headers.TryAddWithoutValidation("X-Token", currentTokenAnswer);
|
authRequest.Headers.TryAddWithoutValidation("X-Token", currentTokenAnswer);
|
||||||
|
authRequest.Headers.TryAddWithoutValidation("DPoP", CreateDpopProof(dpopKey, "POST", AuthenticationPath));
|
||||||
|
|
||||||
using var authResponse = await SendLoginRequestAsync(authRequest, "/api/auth/authentication", cancellationToken);
|
using var authResponse = await SendLoginRequestAsync(authRequest, AuthenticationPath, cancellationToken);
|
||||||
if (authResponse.IsSuccessStatusCode)
|
if (authResponse.IsSuccessStatusCode)
|
||||||
{
|
{
|
||||||
var authBody = await authResponse.Content.ReadAsStringAsync(cancellationToken);
|
var authBody = await authResponse.Content.ReadAsStringAsync(cancellationToken);
|
||||||
@@ -168,6 +173,9 @@ public sealed class GlobalLeaksClient
|
|||||||
|
|
||||||
throw authResponse.StatusCode switch
|
throw authResponse.StatusCode switch
|
||||||
{
|
{
|
||||||
|
HttpStatusCode.Unauthorized when IsInvalidDpopProof(body) => new GlobalLeaksValidationException(
|
||||||
|
"GlobalLeaks ha rechazado la prueba DPoP del login. Revisa la version de GlobalLeaks y el formato del proof.",
|
||||||
|
StatusCodes.Status401Unauthorized),
|
||||||
HttpStatusCode.Unauthorized => new GlobalLeaksValidationException(
|
HttpStatusCode.Unauthorized => new GlobalLeaksValidationException(
|
||||||
"Credenciales incorrectas o codigo 2FA invalido.",
|
"Credenciales incorrectas o codigo 2FA invalido.",
|
||||||
StatusCodes.Status401Unauthorized),
|
StatusCodes.Status401Unauthorized),
|
||||||
@@ -291,13 +299,65 @@ public sealed class GlobalLeaksClient
|
|||||||
ReminderDate = t.ReminderDate,
|
ReminderDate = t.ReminderDate,
|
||||||
AccessDate = t.AccessDate,
|
AccessDate = t.AccessDate,
|
||||||
LastAccess = t.LastAccess,
|
LastAccess = t.LastAccess,
|
||||||
|
WhistleblowerLastAccess = t.WhistleblowerLastAccess,
|
||||||
Status = t.Status,
|
Status = t.Status,
|
||||||
Updated = t.Updated,
|
Updated = t.Updated,
|
||||||
|
Accessible = t.Accessible,
|
||||||
Label = t.Label,
|
Label = t.Label,
|
||||||
})
|
})
|
||||||
.ToArray();
|
.ToArray();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task<IReadOnlyList<ReportDto>> EnrichReportsWithActivityAsync(
|
||||||
|
string sessionId,
|
||||||
|
IReadOnlyList<ReportDto> reports,
|
||||||
|
DateTimeOffset? fallbackReference,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
if (reports.Count == 0)
|
||||||
|
{
|
||||||
|
return reports;
|
||||||
|
}
|
||||||
|
|
||||||
|
var enriched = reports.ToArray();
|
||||||
|
var candidates = reports
|
||||||
|
.Select((report, index) => (Report: report, Index: index))
|
||||||
|
.Where(item => ShouldAnalyzeCitizenActivity(item.Report))
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
if (candidates.Count == 0)
|
||||||
|
{
|
||||||
|
return enriched;
|
||||||
|
}
|
||||||
|
|
||||||
|
using var gate = new SemaphoreSlim(ActivityAnalysisMaxParallelism, ActivityAnalysisMaxParallelism);
|
||||||
|
var tasks = candidates.Select(async item =>
|
||||||
|
{
|
||||||
|
await gate.WaitAsync(cancellationToken);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var reference = GetActivityReference(item.Report, fallbackReference);
|
||||||
|
var activity = await AnalyzeReportActivityAsync(sessionId, item.Report, reference, cancellationToken);
|
||||||
|
enriched[item.Index] = ApplyActivity(item.Report, activity);
|
||||||
|
}
|
||||||
|
catch (Exception ex) when (ex is GlobalLeaksValidationException or HttpRequestException or TaskCanceledException)
|
||||||
|
{
|
||||||
|
_logger.LogWarning(
|
||||||
|
ex,
|
||||||
|
"No se ha podido analizar la actividad ciudadano/OAAF de la denuncia {Progressive} ({ReportId}).",
|
||||||
|
item.Report.Progressive,
|
||||||
|
item.Report.Id);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
gate.Release();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
await Task.WhenAll(tasks);
|
||||||
|
return enriched;
|
||||||
|
}
|
||||||
|
|
||||||
public async Task<ReportDetailDto> GetReportDetailAsync(
|
public async Task<ReportDetailDto> GetReportDetailAsync(
|
||||||
string sessionId,
|
string sessionId,
|
||||||
string reportId,
|
string reportId,
|
||||||
@@ -306,23 +366,25 @@ public sealed class GlobalLeaksClient
|
|||||||
{
|
{
|
||||||
ValidateUuid(reportId);
|
ValidateUuid(reportId);
|
||||||
|
|
||||||
using var detailRequest = CreateAuthenticatedRequest(HttpMethod.Get, $"/api/recipient/rtips/{reportId}", sessionId);
|
var reference = ParseDate(lastAccess);
|
||||||
using var detailResponse = await SendGlRequestAsync(detailRequest, cancellationToken);
|
using var document = await ReadReportDetailDocumentAsync(sessionId, reportId, cancellationToken);
|
||||||
|
IReadOnlyList<ReportCommentDto>? comments = null;
|
||||||
var contentType = detailResponse.Content.Headers.ContentType?.MediaType ?? string.Empty;
|
try
|
||||||
var content = await detailResponse.Content.ReadAsByteArrayAsync(cancellationToken);
|
|
||||||
if (!contentType.Contains("json", StringComparison.OrdinalIgnoreCase) || content.Length == 0)
|
|
||||||
{
|
{
|
||||||
throw new GlobalLeaksValidationException(
|
comments = await GetReportCommentsAsync(sessionId, reportId, reference, defaultNewWhenNoReference: true, cancellationToken);
|
||||||
"No se pudo leer el detalle de la denuncia. Puede estar cifrada sin clave disponible en el servidor.",
|
}
|
||||||
422);
|
catch (Exception ex) when (ex is GlobalLeaksValidationException or HttpRequestException or TaskCanceledException)
|
||||||
|
{
|
||||||
|
_logger.LogWarning(
|
||||||
|
ex,
|
||||||
|
"No se han podido leer los comentarios dedicados de la denuncia {ReportId}. Se intentara usar el detalle del tip.",
|
||||||
|
reportId);
|
||||||
}
|
}
|
||||||
|
|
||||||
using var document = JsonDocument.Parse(content);
|
return ParseReportDetail(reportId, lastAccess, document.RootElement, comments);
|
||||||
return ParseReportDetail(reportId, lastAccess, document.RootElement);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<FileDownloadResult> DownloadReportZipAsync(
|
public async Task<FileDownloadResult> DownloadReportPackageAsync(
|
||||||
string sessionId,
|
string sessionId,
|
||||||
string reportId,
|
string reportId,
|
||||||
CancellationToken cancellationToken)
|
CancellationToken cancellationToken)
|
||||||
@@ -347,7 +409,7 @@ public sealed class GlobalLeaksClient
|
|||||||
|
|
||||||
var fileName = SanitizeFileName(
|
var fileName = SanitizeFileName(
|
||||||
ExtractFileName(response.Content.Headers.ContentDisposition),
|
ExtractFileName(response.Content.Headers.ContentDisposition),
|
||||||
$"report-{reportId}.zip");
|
$"report-{reportId}");
|
||||||
|
|
||||||
return new FileDownloadResult(content, fileName);
|
return new FileDownloadResult(content, fileName);
|
||||||
}
|
}
|
||||||
@@ -379,6 +441,164 @@ public sealed class GlobalLeaksClient
|
|||||||
return new FileDownloadResult(content, $"report-{progressive}.json");
|
return new FileDownloadResult(content, $"report-{progressive}.json");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async Task<JsonDocument> ReadReportDetailDocumentAsync(
|
||||||
|
string sessionId,
|
||||||
|
string reportId,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
using var detailRequest = CreateAuthenticatedRequest(HttpMethod.Get, $"/api/recipient/rtips/{reportId}", sessionId);
|
||||||
|
using var detailResponse = await SendGlRequestAsync(detailRequest, cancellationToken);
|
||||||
|
|
||||||
|
var contentType = detailResponse.Content.Headers.ContentType?.MediaType ?? string.Empty;
|
||||||
|
var content = await detailResponse.Content.ReadAsByteArrayAsync(cancellationToken);
|
||||||
|
if (!contentType.Contains("json", StringComparison.OrdinalIgnoreCase) || content.Length == 0)
|
||||||
|
{
|
||||||
|
throw new GlobalLeaksValidationException(
|
||||||
|
"No se pudo leer el detalle de la denuncia. Puede estar cifrada sin clave disponible en el servidor.",
|
||||||
|
422);
|
||||||
|
}
|
||||||
|
|
||||||
|
return JsonDocument.Parse(content);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<IReadOnlyList<ReportCommentDto>> GetReportCommentsAsync(
|
||||||
|
string sessionId,
|
||||||
|
string reportId,
|
||||||
|
DateTimeOffset? reference,
|
||||||
|
bool defaultNewWhenNoReference,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
using var request = CreateAuthenticatedRequest(HttpMethod.Get, $"/api/recipient/rtips/{reportId}/comments", sessionId);
|
||||||
|
using var response = await SendGlRequestAsync(request, cancellationToken);
|
||||||
|
|
||||||
|
var contentType = response.Content.Headers.ContentType?.MediaType ?? string.Empty;
|
||||||
|
var content = await response.Content.ReadAsByteArrayAsync(cancellationToken);
|
||||||
|
if (!contentType.Contains("json", StringComparison.OrdinalIgnoreCase) || content.Length == 0)
|
||||||
|
{
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
using var document = JsonDocument.Parse(content);
|
||||||
|
var root = document.RootElement;
|
||||||
|
var comments = root.ValueKind == JsonValueKind.Array
|
||||||
|
? root
|
||||||
|
: FindArrayProperty(root, "comments", "items", "data", "entries", "results");
|
||||||
|
|
||||||
|
if (comments is null || comments.Value.ValueKind != JsonValueKind.Array)
|
||||||
|
{
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
return comments.Value
|
||||||
|
.EnumerateArray()
|
||||||
|
.Select(item => CreateReportComment(item, reference, defaultNewWhenNoReference))
|
||||||
|
.ToArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<ReportActivitySnapshot> AnalyzeReportActivityAsync(
|
||||||
|
string sessionId,
|
||||||
|
ReportDto report,
|
||||||
|
DateTimeOffset? reference,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var reportId = report.Id;
|
||||||
|
using var document = await ReadReportDetailDocumentAsync(sessionId, reportId, cancellationToken);
|
||||||
|
IReadOnlyList<ReportCommentDto> comments;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
comments = await GetReportCommentsAsync(
|
||||||
|
sessionId,
|
||||||
|
reportId,
|
||||||
|
reference,
|
||||||
|
defaultNewWhenNoReference: false,
|
||||||
|
cancellationToken);
|
||||||
|
}
|
||||||
|
catch (Exception ex) when (ex is GlobalLeaksValidationException or HttpRequestException or TaskCanceledException)
|
||||||
|
{
|
||||||
|
_logger.LogWarning(
|
||||||
|
ex,
|
||||||
|
"No se han podido leer los comentarios dedicados para analizar actividad de la denuncia {ReportId}. Se usara el detalle del tip si contiene comentarios.",
|
||||||
|
reportId);
|
||||||
|
comments = EnumerateArray(document.RootElement, "comments")
|
||||||
|
.Select(item => CreateReportComment(item, reference, defaultNewWhenNoReference: false))
|
||||||
|
.ToArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
var whistleblowerFiles = ParseReportFiles(
|
||||||
|
document.RootElement,
|
||||||
|
reference,
|
||||||
|
defaultNewWhenNoReference: false,
|
||||||
|
"wbfiles",
|
||||||
|
"files");
|
||||||
|
var receiverFiles = ParseReportFiles(
|
||||||
|
document.RootElement,
|
||||||
|
reference,
|
||||||
|
defaultNewWhenNoReference: false,
|
||||||
|
"rfiles");
|
||||||
|
|
||||||
|
var citizenCommentDates = comments
|
||||||
|
.Where(comment => IsWhistleblowerActivityType(comment.Type))
|
||||||
|
.Select(comment => ParseDate(comment.CreationDate))
|
||||||
|
.Where(date => date is not null)
|
||||||
|
.Select(date => date!.Value);
|
||||||
|
var receiverCommentDates = comments
|
||||||
|
.Where(comment => IsReceiverActivityType(comment.Type))
|
||||||
|
.Select(comment => ParseDate(comment.CreationDate))
|
||||||
|
.Where(date => date is not null)
|
||||||
|
.Select(date => date!.Value);
|
||||||
|
var unclassifiedCommentDates = comments
|
||||||
|
.Where(comment => IsUnclassifiedActivityType(comment.Type))
|
||||||
|
.Select(comment => ParseDate(comment.CreationDate))
|
||||||
|
.Where(date => date is not null)
|
||||||
|
.Select(date => date!.Value);
|
||||||
|
var citizenFileDates = whistleblowerFiles
|
||||||
|
.Select(file => ParseDate(file.CreationDate))
|
||||||
|
.Where(date => date is not null)
|
||||||
|
.Select(date => date!.Value);
|
||||||
|
var receiverFileDates = receiverFiles
|
||||||
|
.Select(file => ParseDate(file.CreationDate))
|
||||||
|
.Where(date => date is not null)
|
||||||
|
.Select(date => date!.Value);
|
||||||
|
|
||||||
|
var reportCreationDate = ParseDate(report.CreationDate) ??
|
||||||
|
ParseDate(GetString(document.RootElement, "creation_date", "creationDate"));
|
||||||
|
var citizenActivityDates = reportCreationDate is null
|
||||||
|
? citizenCommentDates.Concat(citizenFileDates)
|
||||||
|
: citizenCommentDates.Concat(citizenFileDates).Append(reportCreationDate.Value);
|
||||||
|
|
||||||
|
var citizenLast = MaxDate(citizenActivityDates);
|
||||||
|
var receiverLast = MaxDate(receiverCommentDates.Concat(receiverFileDates).Concat(unclassifiedCommentDates));
|
||||||
|
|
||||||
|
return new ReportActivitySnapshot(
|
||||||
|
citizenLast,
|
||||||
|
comments.Any(comment =>
|
||||||
|
IsWhistleblowerActivityType(comment.Type) &&
|
||||||
|
comment.IsNew),
|
||||||
|
whistleblowerFiles.Any(file => file.IsNew),
|
||||||
|
receiverLast,
|
||||||
|
comments.Any(comment =>
|
||||||
|
IsReceiverActivityType(comment.Type) &&
|
||||||
|
comment.IsNew) ||
|
||||||
|
comments.Any(comment =>
|
||||||
|
IsUnclassifiedActivityType(comment.Type) &&
|
||||||
|
comment.IsNew) ||
|
||||||
|
receiverFiles.Any(file => file.IsNew));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static ReportDto ApplyActivity(ReportDto report, ReportActivitySnapshot activity)
|
||||||
|
{
|
||||||
|
return report with
|
||||||
|
{
|
||||||
|
ActivityAnalyzed = true,
|
||||||
|
CitizenLastActivity = activity.CitizenLastActivity?.ToString("O", CultureInfo.InvariantCulture),
|
||||||
|
CitizenHasNewActivity = activity.HasNewCitizenComment || activity.HasNewCitizenFile,
|
||||||
|
CitizenHasNewComment = activity.HasNewCitizenComment,
|
||||||
|
CitizenHasNewFile = activity.HasNewCitizenFile,
|
||||||
|
ReceiverLastActivity = activity.ReceiverLastActivity?.ToString("O", CultureInfo.InvariantCulture),
|
||||||
|
ReceiverHasNewActivity = activity.HasNewReceiverActivity
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
private async Task<HttpResponseMessage> SendLoginRequestAsync(
|
private async Task<HttpResponseMessage> SendLoginRequestAsync(
|
||||||
HttpRequestMessage request,
|
HttpRequestMessage request,
|
||||||
string endpoint,
|
string endpoint,
|
||||||
@@ -497,6 +717,49 @@ public sealed class GlobalLeaksClient
|
|||||||
return Convert.ToBase64String(hash);
|
return Convert.ToBase64String(hash);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static string CreateDpopProof(ECDsa privateKey, string method, string path)
|
||||||
|
{
|
||||||
|
var publicParameters = privateKey.ExportParameters(includePrivateParameters: false);
|
||||||
|
var jwk = new Dictionary<string, object?>
|
||||||
|
{
|
||||||
|
["kty"] = "EC",
|
||||||
|
["crv"] = "P-256",
|
||||||
|
["x"] = Base64UrlEncode(publicParameters.Q.X ?? []),
|
||||||
|
["y"] = Base64UrlEncode(publicParameters.Q.Y ?? []),
|
||||||
|
};
|
||||||
|
|
||||||
|
var header = new Dictionary<string, object?>
|
||||||
|
{
|
||||||
|
["typ"] = "dpop+jwt",
|
||||||
|
["alg"] = "ES256",
|
||||||
|
["jwk"] = jwk,
|
||||||
|
};
|
||||||
|
|
||||||
|
var payload = new Dictionary<string, object?>
|
||||||
|
{
|
||||||
|
["jti"] = Guid.NewGuid().ToString(),
|
||||||
|
["htm"] = method,
|
||||||
|
["htu"] = path,
|
||||||
|
["iat"] = DateTimeOffset.UtcNow.ToUnixTimeSeconds(),
|
||||||
|
};
|
||||||
|
|
||||||
|
var encodedHeader = Base64UrlEncode(JsonSerializer.SerializeToUtf8Bytes(header, JsonOptions));
|
||||||
|
var encodedPayload = Base64UrlEncode(JsonSerializer.SerializeToUtf8Bytes(payload, JsonOptions));
|
||||||
|
var signingInput = $"{encodedHeader}.{encodedPayload}";
|
||||||
|
var signature = privateKey.SignData(
|
||||||
|
Encoding.ASCII.GetBytes(signingInput),
|
||||||
|
HashAlgorithmName.SHA256,
|
||||||
|
DSASignatureFormat.IeeeP1363FixedFieldConcatenation);
|
||||||
|
|
||||||
|
return $"{signingInput}.{Base64UrlEncode(signature)}";
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string Base64UrlEncode(byte[] bytes)
|
||||||
|
=> Convert.ToBase64String(bytes)
|
||||||
|
.TrimEnd('=')
|
||||||
|
.Replace('+', '-')
|
||||||
|
.Replace('/', '_');
|
||||||
|
|
||||||
private static byte[] ComputeArgon2(byte[] input, byte[] salt, int iterations, int memoryKb)
|
private static byte[] ComputeArgon2(byte[] input, byte[] salt, int iterations, int memoryKb)
|
||||||
{
|
{
|
||||||
var argon2 = new Argon2id(input)
|
var argon2 = new Argon2id(input)
|
||||||
@@ -530,6 +793,34 @@ public sealed class GlobalLeaksClient
|
|||||||
: null;
|
: null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static bool ShouldAnalyzeCitizenActivity(ReportDto report)
|
||||||
|
{
|
||||||
|
return report.Accessible != false && !string.IsNullOrWhiteSpace(report.Id);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static DateTimeOffset? GetActivityReference(ReportDto report, DateTimeOffset? fallbackReference)
|
||||||
|
{
|
||||||
|
return ParseDate(report.LastDownloadedAt) ??
|
||||||
|
fallbackReference ??
|
||||||
|
ParseDate(report.LastAccess) ??
|
||||||
|
ParseDate(report.AccessDate) ??
|
||||||
|
ParseDate(report.CreationDate);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static DateTimeOffset? MaxDate(IEnumerable<DateTimeOffset> values)
|
||||||
|
{
|
||||||
|
DateTimeOffset? max = null;
|
||||||
|
foreach (var value in values)
|
||||||
|
{
|
||||||
|
if (max is null || value > max.Value)
|
||||||
|
{
|
||||||
|
max = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return max;
|
||||||
|
}
|
||||||
|
|
||||||
private static string ExtractName(JsonElement? name, string fallback)
|
private static string ExtractName(JsonElement? name, string fallback)
|
||||||
{
|
{
|
||||||
if (name is null)
|
if (name is null)
|
||||||
@@ -636,8 +927,10 @@ public sealed class GlobalLeaksClient
|
|||||||
ReminderDate = GetString(item, "reminder_date", "reminderDate"),
|
ReminderDate = GetString(item, "reminder_date", "reminderDate"),
|
||||||
AccessDate = GetString(item, "access_date", "accessDate"),
|
AccessDate = GetString(item, "access_date", "accessDate"),
|
||||||
LastAccess = GetString(item, "last_access", "lastAccess"),
|
LastAccess = GetString(item, "last_access", "lastAccess"),
|
||||||
|
WhistleblowerLastAccess = GetString(item, "wb_last_access", "wbLastAccess"),
|
||||||
Status = GetString(item, "status"),
|
Status = GetString(item, "status"),
|
||||||
Updated = GetBool(item, "updated"),
|
Updated = GetBool(item, "updated"),
|
||||||
|
Accessible = GetNullableBool(item, "accessible"),
|
||||||
Label = GetString(item, "label"),
|
Label = GetString(item, "label"),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -645,51 +938,190 @@ public sealed class GlobalLeaksClient
|
|||||||
return reports;
|
return reports;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static ReportDetailDto ParseReportDetail(string reportId, string? lastAccess, JsonElement root)
|
private static ReportDetailDto ParseReportDetail(
|
||||||
|
string reportId,
|
||||||
|
string? lastAccess,
|
||||||
|
JsonElement root,
|
||||||
|
IReadOnlyList<ReportCommentDto>? commentsOverride = null)
|
||||||
{
|
{
|
||||||
var lastAccessDate = ParseDate(lastAccess);
|
var lastAccessDate = ParseDate(lastAccess);
|
||||||
|
|
||||||
bool IsNew(string? value)
|
var comments = commentsOverride ?? EnumerateArray(root, "comments")
|
||||||
|
.Select(item => CreateReportComment(item, lastAccessDate, defaultNewWhenNoReference: true))
|
||||||
|
.ToArray();
|
||||||
|
|
||||||
|
var whistleblowerFiles = ParseReportFiles(root, lastAccessDate, defaultNewWhenNoReference: true, "wbfiles", "files");
|
||||||
|
var receiverFiles = ParseReportFiles(root, lastAccessDate, defaultNewWhenNoReference: true, "rfiles");
|
||||||
|
|
||||||
|
return new ReportDetailDto(reportId, lastAccess, comments, whistleblowerFiles, receiverFiles);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static ReportCommentDto CreateReportComment(
|
||||||
|
JsonElement item,
|
||||||
|
DateTimeOffset? reference,
|
||||||
|
bool defaultNewWhenNoReference)
|
||||||
{
|
{
|
||||||
if (lastAccessDate is null)
|
var creationDate = GetString(item, "creation_date", "creationDate", "date", "created_at", "createdAt");
|
||||||
|
return new ReportCommentDto(
|
||||||
|
GetString(item, "id"),
|
||||||
|
GetCommentActivityType(item),
|
||||||
|
GetString(item, "content", "text", "message"),
|
||||||
|
creationDate,
|
||||||
|
IsAfterReference(creationDate, reference, defaultNewWhenNoReference));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string? GetCommentActivityType(JsonElement item)
|
||||||
{
|
{
|
||||||
return true;
|
if (GetBool(item, "whistleblower", "from_whistleblower", "fromWhistleblower", "is_whistleblower", "isWhistleblower"))
|
||||||
|
{
|
||||||
|
return "whistleblower";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (GetBool(item, "receiver", "recipient", "from_receiver", "fromReceiver", "is_receiver", "isReceiver", "admin", "operator", "staff"))
|
||||||
|
{
|
||||||
|
return "receiver";
|
||||||
|
}
|
||||||
|
|
||||||
|
var direct = GetString(
|
||||||
|
item,
|
||||||
|
"type",
|
||||||
|
"kind",
|
||||||
|
"author_type",
|
||||||
|
"authorType",
|
||||||
|
"author_role",
|
||||||
|
"authorRole",
|
||||||
|
"role",
|
||||||
|
"user_role",
|
||||||
|
"userRole",
|
||||||
|
"sender",
|
||||||
|
"sender_type",
|
||||||
|
"senderType",
|
||||||
|
"source",
|
||||||
|
"actor",
|
||||||
|
"creator",
|
||||||
|
"from");
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(direct))
|
||||||
|
{
|
||||||
|
return direct;
|
||||||
|
}
|
||||||
|
|
||||||
|
return GetNestedCommentActivityType(item, "author", "user", "creator", "sender", "actor", "owner");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string? GetNestedCommentActivityType(JsonElement item, params string[] names)
|
||||||
|
{
|
||||||
|
foreach (var name in names)
|
||||||
|
{
|
||||||
|
if (!item.TryGetProperty(name, out var property))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (property.ValueKind == JsonValueKind.String)
|
||||||
|
{
|
||||||
|
return property.GetString();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (property.ValueKind != JsonValueKind.Object)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var nested = GetString(
|
||||||
|
property,
|
||||||
|
"type",
|
||||||
|
"kind",
|
||||||
|
"role",
|
||||||
|
"user_role",
|
||||||
|
"userRole",
|
||||||
|
"author_type",
|
||||||
|
"authorType",
|
||||||
|
"name",
|
||||||
|
"username",
|
||||||
|
"display_name",
|
||||||
|
"displayName");
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(nested))
|
||||||
|
{
|
||||||
|
return nested;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool IsWhistleblowerActivityType(string? value)
|
||||||
|
{
|
||||||
|
return MatchesAny(value, "whistleblower", "citizen", "source", "tipper", "submitter", "denunciante");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool IsReceiverActivityType(string? value)
|
||||||
|
{
|
||||||
|
return MatchesAny(value, "receiver", "recipient", "admin", "administrator", "operator", "staff", "custodian", "moderator", "gestor", "oaaf");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool IsUnclassifiedActivityType(string? value)
|
||||||
|
{
|
||||||
|
return !IsWhistleblowerActivityType(value) && !IsReceiverActivityType(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
{
|
||||||
|
return Regex.Replace(value.Trim().ToLowerInvariant(), @"[^a-z0-9]+", " ").Trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static ReportFileDto[] ParseReportFiles(
|
||||||
|
JsonElement root,
|
||||||
|
DateTimeOffset? reference,
|
||||||
|
bool defaultNewWhenNoReference,
|
||||||
|
params string[] propertyNames)
|
||||||
|
{
|
||||||
|
return EnumerateArray(root, propertyNames)
|
||||||
|
.Select(item =>
|
||||||
|
{
|
||||||
|
var creationDate = GetString(item, "creation_date", "creationDate");
|
||||||
|
return new ReportFileDto(
|
||||||
|
GetString(item, "id"),
|
||||||
|
GetLocalizedString(item, "name", "file_name", "filename"),
|
||||||
|
GetInt64(item, "size"),
|
||||||
|
GetString(item, "content_type", "contentType", "mime_type", "mimetype"),
|
||||||
|
creationDate,
|
||||||
|
IsAfterReference(creationDate, reference, defaultNewWhenNoReference));
|
||||||
|
})
|
||||||
|
.ToArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool IsAfterReference(
|
||||||
|
string? value,
|
||||||
|
DateTimeOffset? reference,
|
||||||
|
bool defaultWhenNoReference)
|
||||||
|
{
|
||||||
|
if (reference is null)
|
||||||
|
{
|
||||||
|
return defaultWhenNoReference;
|
||||||
}
|
}
|
||||||
|
|
||||||
var itemDate = ParseDate(value);
|
var itemDate = ParseDate(value);
|
||||||
return itemDate is not null && itemDate > lastAccessDate;
|
return itemDate is not null && itemDate > reference;
|
||||||
}
|
|
||||||
|
|
||||||
var comments = EnumerateArray(root, "comments")
|
|
||||||
.Select(item => new ReportCommentDto(
|
|
||||||
GetString(item, "id"),
|
|
||||||
GetString(item, "type"),
|
|
||||||
GetString(item, "content", "text", "message"),
|
|
||||||
GetString(item, "creation_date", "creationDate"),
|
|
||||||
IsNew(GetString(item, "creation_date", "creationDate"))))
|
|
||||||
.ToArray();
|
|
||||||
|
|
||||||
var whistleblowerFiles = EnumerateArray(root, "wbfiles", "files")
|
|
||||||
.Select(item => new ReportFileDto(
|
|
||||||
GetString(item, "id"),
|
|
||||||
GetLocalizedString(item, "name", "file_name", "filename"),
|
|
||||||
GetInt64(item, "size"),
|
|
||||||
GetString(item, "content_type", "contentType", "mime_type", "mimetype"),
|
|
||||||
GetString(item, "creation_date", "creationDate"),
|
|
||||||
IsNew(GetString(item, "creation_date", "creationDate"))))
|
|
||||||
.ToArray();
|
|
||||||
|
|
||||||
var receiverFiles = EnumerateArray(root, "rfiles")
|
|
||||||
.Select(item => new ReportFileDto(
|
|
||||||
GetString(item, "id"),
|
|
||||||
GetLocalizedString(item, "name", "file_name", "filename"),
|
|
||||||
GetInt64(item, "size"),
|
|
||||||
GetString(item, "content_type", "contentType", "mime_type", "mimetype"),
|
|
||||||
GetString(item, "creation_date", "creationDate"),
|
|
||||||
IsNew(GetString(item, "creation_date", "creationDate"))))
|
|
||||||
.ToArray();
|
|
||||||
|
|
||||||
return new ReportDetailDto(reportId, lastAccess, comments, whistleblowerFiles, receiverFiles);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static IEnumerable<JsonElement> EnumerateArray(JsonElement root, params string[] names)
|
private static IEnumerable<JsonElement> EnumerateArray(JsonElement root, params string[] names)
|
||||||
@@ -839,6 +1271,28 @@ public sealed class GlobalLeaksClient
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static bool? GetNullableBool(JsonElement element, params string[] names)
|
||||||
|
{
|
||||||
|
foreach (var name in names)
|
||||||
|
{
|
||||||
|
if (element.TryGetProperty(name, out var property))
|
||||||
|
{
|
||||||
|
if (property.ValueKind is JsonValueKind.True or JsonValueKind.False)
|
||||||
|
{
|
||||||
|
return property.GetBoolean();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (property.ValueKind == JsonValueKind.String &&
|
||||||
|
bool.TryParse(property.GetString(), out var value))
|
||||||
|
{
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
private static string SanitizeFileName(string? name, string fallback)
|
private static string SanitizeFileName(string? name, string fallback)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrWhiteSpace(name))
|
if (string.IsNullOrWhiteSpace(name))
|
||||||
@@ -922,8 +1376,20 @@ public sealed class GlobalLeaksClient
|
|||||||
body.Contains("Invalid request: No token", StringComparison.OrdinalIgnoreCase));
|
body.Contains("Invalid request: No token", StringComparison.OrdinalIgnoreCase));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static bool IsInvalidDpopProof(string body)
|
||||||
|
{
|
||||||
|
return !string.IsNullOrWhiteSpace(body) &&
|
||||||
|
body.Contains("Invalid DPoP proof", StringComparison.OrdinalIgnoreCase);
|
||||||
|
}
|
||||||
|
|
||||||
private sealed record TokenResponse(string Id, string Salt);
|
private sealed record TokenResponse(string Id, string Salt);
|
||||||
private sealed record AuthTypeResponse(string Type, string Salt);
|
private sealed record AuthTypeResponse(string Type, string Salt);
|
||||||
|
private sealed record ReportActivitySnapshot(
|
||||||
|
DateTimeOffset? CitizenLastActivity,
|
||||||
|
bool HasNewCitizenComment,
|
||||||
|
bool HasNewCitizenFile,
|
||||||
|
DateTimeOffset? ReceiverLastActivity,
|
||||||
|
bool HasNewReceiverActivity);
|
||||||
|
|
||||||
private sealed record RawReport
|
private sealed record RawReport
|
||||||
{
|
{
|
||||||
@@ -936,8 +1402,10 @@ public sealed class GlobalLeaksClient
|
|||||||
public string? ReminderDate { get; init; }
|
public string? ReminderDate { get; init; }
|
||||||
public string? AccessDate { get; init; }
|
public string? AccessDate { get; init; }
|
||||||
public string? LastAccess { get; init; }
|
public string? LastAccess { get; init; }
|
||||||
|
public string? WhistleblowerLastAccess { get; init; }
|
||||||
public string? Status { get; init; }
|
public string? Status { get; init; }
|
||||||
public bool Updated { get; init; }
|
public bool Updated { get; init; }
|
||||||
|
public bool? Accessible { get; init; }
|
||||||
public string? Label { get; init; }
|
public string? Label { get; init; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ namespace ApiDenuncias.Services;
|
|||||||
|
|
||||||
public sealed class GlobalLeaksSessionStore
|
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)
|
private static readonly JsonSerializerOptions JsonOptions = new(JsonSerializerDefaults.Web)
|
||||||
{
|
{
|
||||||
WriteIndented = false,
|
WriteIndented = false,
|
||||||
|
|||||||
@@ -16,4 +16,8 @@ public interface IFilteredDenunciaStore
|
|||||||
Task<List<FicherosDenuncias>> GetFicherosByDenunciaIdsAsync(
|
Task<List<FicherosDenuncias>> GetFicherosByDenunciaIdsAsync(
|
||||||
IReadOnlyCollection<int> denunciaIds,
|
IReadOnlyCollection<int> denunciaIds,
|
||||||
CancellationToken cancellationToken = default);
|
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;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
@@ -12,26 +12,31 @@ namespace ApiDenuncias.Services
|
|||||||
// =========================
|
// =========================
|
||||||
|
|
||||||
/// <summary>
|
/// <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>
|
/// </summary>
|
||||||
Task<GestionaCreateFileResponse> CreateFileAsync(
|
Task<GestionaCreateFileResponse> CreateFileAsync(
|
||||||
Guid procedureId,
|
|
||||||
string subject,
|
string subject,
|
||||||
string documentSeries,
|
string documentSeries,
|
||||||
string siaCode
|
string siaCode
|
||||||
);
|
);
|
||||||
|
|
||||||
/// <summary>
|
/// <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>
|
/// </summary>
|
||||||
Task OpenFileAsync(
|
Task OpenFileAsync(
|
||||||
string fileUrl,
|
string fileUrl,
|
||||||
string? fileOpenUrl,
|
string? fileOpenUrl,
|
||||||
Guid managementUnitGroupId,
|
string assignedGroupCode,
|
||||||
Guid assignedGroupId,
|
|
||||||
bool confidential,
|
bool confidential,
|
||||||
string freeTitle,
|
string freeTitle
|
||||||
string siaCode
|
);
|
||||||
|
|
||||||
|
/// <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>
|
/// <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>
|
/// </summary>
|
||||||
Task UploadDocumentAsync(
|
Task UploadDocumentAsync(
|
||||||
string fileUrl,
|
string fileUrl,
|
||||||
@@ -56,7 +61,7 @@ namespace ApiDenuncias.Services
|
|||||||
);
|
);
|
||||||
|
|
||||||
/// <summary>
|
/// <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>
|
/// </summary>
|
||||||
Task<Guid> CreateFolderAsync(
|
Task<Guid> CreateFolderAsync(
|
||||||
string fileUrl,
|
string fileUrl,
|
||||||
@@ -89,11 +94,11 @@ namespace ApiDenuncias.Services
|
|||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Usa el NIF tal cual viene.
|
/// 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 existe, lo crea.
|
||||||
/// Si no est<EFBFBD> enlazado al expediente, lo enlaza.
|
/// Si no está enlazado al expediente, lo enlaza.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
Task AsegurarTerceroYEnlazarAsync(string fileUrl, ThirdPartyIdentityData thirdParty);
|
Task<GestionaEnsureThirdResponse> AsegurarTerceroYEnlazarAsync(string fileUrl, ThirdPartyIdentityData thirdParty);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -102,21 +107,31 @@ namespace ApiDenuncias.Services
|
|||||||
// =========================
|
// =========================
|
||||||
|
|
||||||
/// <summary>
|
/// <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>
|
/// </summary>
|
||||||
Task<string> ListarExpedientesJsonAsyncBasico(int maxPages = 1);
|
Task<string> ListarExpedientesJsonAsyncBasico(int maxPages = 1);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Busca directamente un expediente cuyo asunto sea "Denuncia {idDenuncia}-CD".
|
/// 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>
|
/// </summary>
|
||||||
Task<GestionaExpedienteInfo?> BuscarExpedientePorIdEnAsuntoAsync(int idDenuncia);
|
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>
|
/// <summary>
|
||||||
/// Obtiene los metadatos visibles de un expediente concreto.
|
/// Obtiene los metadatos visibles de un expediente concreto.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
Task<GestionaExpedienteInfo?> ObtenerExpedienteAsync(string fileUrl);
|
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(
|
Task<List<ExpedienteTerceroDto>> ObtenerExpedientesPorTerceroAsync(
|
||||||
string nif,
|
string nif,
|
||||||
DateTimeOffset? desde = null,
|
DateTimeOffset? desde = null,
|
||||||
@@ -130,3 +145,5 @@ namespace ApiDenuncias.Services
|
|||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -262,6 +262,106 @@ 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,
|
||||||
|
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)
|
private async Task<long> EnsureUserAsync(MySqlConnection connection, string username, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
const string insertSql = """
|
const string insertSql = """
|
||||||
|
|||||||
@@ -29,6 +29,26 @@ public sealed class MySqlDenunciaStore : IDenunciaStore
|
|||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci
|
) 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 = """
|
private const string ComplaintSelectColumns = """
|
||||||
external_registry_id,
|
external_registry_id,
|
||||||
external_report_id,
|
external_report_id,
|
||||||
@@ -94,6 +114,8 @@ public sealed class MySqlDenunciaStore : IDenunciaStore
|
|||||||
workflow_status,
|
workflow_status,
|
||||||
selected_document_name,
|
selected_document_name,
|
||||||
gestiona_uploaded_at_utc,
|
gestiona_uploaded_at_utc,
|
||||||
|
gestiona_last_upload_type,
|
||||||
|
gestiona_assigned_group,
|
||||||
is_in_gestiona,
|
is_in_gestiona,
|
||||||
is_rejected,
|
is_rejected,
|
||||||
key_date,
|
key_date,
|
||||||
@@ -118,6 +140,23 @@ public sealed class MySqlDenunciaStore : IDenunciaStore
|
|||||||
a.encrypted_at_utc
|
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 =
|
private static readonly (string Table, string Column, string Definition)[] SchemaColumnsToEnsure =
|
||||||
[
|
[
|
||||||
("complaints", "gestiona_file_code", "`gestiona_file_code` VARCHAR(128) NOT NULL DEFAULT ''"),
|
("complaints", "gestiona_file_code", "`gestiona_file_code` VARCHAR(128) NOT NULL DEFAULT ''"),
|
||||||
@@ -143,6 +182,8 @@ public sealed class MySqlDenunciaStore : IDenunciaStore
|
|||||||
("complaints", "key_date", "`key_date` DATE NULL"),
|
("complaints", "key_date", "`key_date` DATE NULL"),
|
||||||
("complaints", "encryption_scheme", "`encryption_scheme` VARCHAR(64) NOT NULL DEFAULT 'none'"),
|
("complaints", "encryption_scheme", "`encryption_scheme` VARCHAR(64) NOT NULL DEFAULT 'none'"),
|
||||||
("complaints", "encrypted_at_utc", "`encrypted_at_utc` DATETIME(6) NULL"),
|
("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 ''"),
|
||||||
("complaint_attachments", "content_sha256", "`content_sha256` CHAR(64) NOT NULL DEFAULT ''"),
|
("complaint_attachments", "content_sha256", "`content_sha256` CHAR(64) NOT NULL DEFAULT ''"),
|
||||||
("complaint_attachments", "key_date", "`key_date` DATE NULL"),
|
("complaint_attachments", "key_date", "`key_date` DATE NULL"),
|
||||||
("complaint_attachments", "encryption_scheme", "`encryption_scheme` VARCHAR(64) NOT NULL DEFAULT 'none'"),
|
("complaint_attachments", "encryption_scheme", "`encryption_scheme` VARCHAR(64) NOT NULL DEFAULT 'none'"),
|
||||||
@@ -211,6 +252,8 @@ public sealed class MySqlDenunciaStore : IDenunciaStore
|
|||||||
("complaints", "`display_name` TEXT NOT NULL"),
|
("complaints", "`display_name` TEXT NOT NULL"),
|
||||||
("complaints", "`workflow_status` TEXT NOT NULL"),
|
("complaints", "`workflow_status` TEXT NOT NULL"),
|
||||||
("complaints", "`selected_document_name` TEXT 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", "`description` TEXT NULL"),
|
||||||
("complaint_attachments", "`notes` TEXT NOT NULL"),
|
("complaint_attachments", "`notes` TEXT NOT NULL"),
|
||||||
];
|
];
|
||||||
@@ -422,6 +465,68 @@ public sealed class MySqlDenunciaStore : IDenunciaStore
|
|||||||
return result;
|
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(
|
public async Task<DenunciasGestiona?> GetDenunciaByIdAsync(
|
||||||
int denunciaId,
|
int denunciaId,
|
||||||
CancellationToken cancellationToken = default)
|
CancellationToken cancellationToken = default)
|
||||||
@@ -446,6 +551,60 @@ public sealed class MySqlDenunciaStore : IDenunciaStore
|
|||||||
: null;
|
: 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)
|
public async Task UpsertDenunciaAsync(DenunciasGestiona denuncia, CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
await EnsureSchemaReadyAsync(cancellationToken);
|
await EnsureSchemaReadyAsync(cancellationToken);
|
||||||
@@ -516,6 +675,8 @@ public sealed class MySqlDenunciaStore : IDenunciaStore
|
|||||||
workflow_status,
|
workflow_status,
|
||||||
selected_document_name,
|
selected_document_name,
|
||||||
gestiona_uploaded_at_utc,
|
gestiona_uploaded_at_utc,
|
||||||
|
gestiona_last_upload_type,
|
||||||
|
gestiona_assigned_group,
|
||||||
is_in_gestiona,
|
is_in_gestiona,
|
||||||
is_rejected,
|
is_rejected,
|
||||||
key_date,
|
key_date,
|
||||||
@@ -586,6 +747,8 @@ public sealed class MySqlDenunciaStore : IDenunciaStore
|
|||||||
@workflowStatus,
|
@workflowStatus,
|
||||||
@selectedDocumentName,
|
@selectedDocumentName,
|
||||||
@gestionaUploadedAtUtc,
|
@gestionaUploadedAtUtc,
|
||||||
|
@gestionaLastUploadType,
|
||||||
|
@gestionaAssignedGroup,
|
||||||
@isInGestiona,
|
@isInGestiona,
|
||||||
@isRejected,
|
@isRejected,
|
||||||
@keyDate,
|
@keyDate,
|
||||||
@@ -656,6 +819,8 @@ public sealed class MySqlDenunciaStore : IDenunciaStore
|
|||||||
workflow_status = VALUES(workflow_status),
|
workflow_status = VALUES(workflow_status),
|
||||||
selected_document_name = VALUES(selected_document_name),
|
selected_document_name = VALUES(selected_document_name),
|
||||||
gestiona_uploaded_at_utc = VALUES(gestiona_uploaded_at_utc),
|
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),
|
||||||
is_in_gestiona = VALUES(is_in_gestiona),
|
is_in_gestiona = VALUES(is_in_gestiona),
|
||||||
is_rejected = VALUES(is_rejected),
|
is_rejected = VALUES(is_rejected),
|
||||||
key_date = VALUES(key_date),
|
key_date = VALUES(key_date),
|
||||||
@@ -731,6 +896,8 @@ public sealed class MySqlDenunciaStore : IDenunciaStore
|
|||||||
command.Parameters.AddWithValue("@workflowStatus", denuncia.EstadoDenuncia ?? string.Empty);
|
command.Parameters.AddWithValue("@workflowStatus", denuncia.EstadoDenuncia ?? string.Empty);
|
||||||
command.Parameters.AddWithValue("@selectedDocumentName", ToDbStringOrNull(denuncia.ArchivoElegido));
|
command.Parameters.AddWithValue("@selectedDocumentName", ToDbStringOrNull(denuncia.ArchivoElegido));
|
||||||
command.Parameters.AddWithValue("@gestionaUploadedAtUtc", ToDbDate(denuncia.FechaSubidaAGestiona));
|
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("@isInGestiona", denuncia.EnGestiona);
|
command.Parameters.AddWithValue("@isInGestiona", denuncia.EnGestiona);
|
||||||
command.Parameters.AddWithValue("@isRejected", denuncia.EnRechazada);
|
command.Parameters.AddWithValue("@isRejected", denuncia.EnRechazada);
|
||||||
command.Parameters.AddWithValue("@keyDate", ToDbDate(denuncia.KeyDate));
|
command.Parameters.AddWithValue("@keyDate", ToDbDate(denuncia.KeyDate));
|
||||||
@@ -799,12 +966,10 @@ public sealed class MySqlDenunciaStore : IDenunciaStore
|
|||||||
content_mime_type = @contentMimeType,
|
content_mime_type = @contentMimeType,
|
||||||
content_sha256 = @contentSha256,
|
content_sha256 = @contentSha256,
|
||||||
uploaded_to_gestiona = CASE
|
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
|
WHEN complaint_attachments.content_sha256 = @contentSha256 THEN complaint_attachments.uploaded_to_gestiona
|
||||||
ELSE @uploadedToGestiona
|
ELSE @uploadedToGestiona
|
||||||
END,
|
END,
|
||||||
uploaded_at_utc = CASE
|
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
|
WHEN complaint_attachments.content_sha256 = @contentSha256 THEN complaint_attachments.uploaded_at_utc
|
||||||
ELSE @uploadedAtUtc
|
ELSE @uploadedAtUtc
|
||||||
END,
|
END,
|
||||||
@@ -1125,6 +1290,7 @@ public sealed class MySqlDenunciaStore : IDenunciaStore
|
|||||||
CancellationToken cancellationToken)
|
CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
await EnsureAttachmentChunksTableAsync(connection, cancellationToken);
|
await EnsureAttachmentChunksTableAsync(connection, cancellationToken);
|
||||||
|
await EnsureGestionaUploadHistoryTableAsync(connection, cancellationToken);
|
||||||
|
|
||||||
foreach (var (table, column, definition) in SchemaColumnsToEnsure)
|
foreach (var (table, column, definition) in SchemaColumnsToEnsure)
|
||||||
{
|
{
|
||||||
@@ -1167,6 +1333,15 @@ public sealed class MySqlDenunciaStore : IDenunciaStore
|
|||||||
await command.ExecuteNonQueryAsync(cancellationToken);
|
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)
|
private static string ExtractColumnName(string definition)
|
||||||
{
|
{
|
||||||
var first = definition.IndexOf('`');
|
var first = definition.IndexOf('`');
|
||||||
@@ -1462,6 +1637,8 @@ public sealed class MySqlDenunciaStore : IDenunciaStore
|
|||||||
EstadoDenuncia = GetString(record, "workflow_status"),
|
EstadoDenuncia = GetString(record, "workflow_status"),
|
||||||
ArchivoElegido = GetString(record, "selected_document_name"),
|
ArchivoElegido = GetString(record, "selected_document_name"),
|
||||||
FechaSubidaAGestiona = GetDateTime(record, "gestiona_uploaded_at_utc"),
|
FechaSubidaAGestiona = GetDateTime(record, "gestiona_uploaded_at_utc"),
|
||||||
|
UltimaSubidaGestionaTipo = GetString(record, "gestiona_last_upload_type"),
|
||||||
|
UltimoGrupoAsignadoGestiona = GetString(record, "gestiona_assigned_group"),
|
||||||
EnGestiona = GetBoolean(record, "is_in_gestiona"),
|
EnGestiona = GetBoolean(record, "is_in_gestiona"),
|
||||||
EnRechazada = GetBoolean(record, "is_rejected"),
|
EnRechazada = GetBoolean(record, "is_rejected"),
|
||||||
KeyDate = GetNullableDateOnly(record, "key_date"),
|
KeyDate = GetNullableDateOnly(record, "key_date"),
|
||||||
@@ -1491,6 +1668,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)
|
private static object ToDbDate(DateTime value)
|
||||||
{
|
{
|
||||||
return value == DateTime.MinValue ? DBNull.Value : value;
|
return value == DateTime.MinValue ? DBNull.Value : value;
|
||||||
|
|||||||
@@ -36,15 +36,19 @@
|
|||||||
"Gestiona": {
|
"Gestiona": {
|
||||||
"ApiBase": "https://02.g3stiona.com",
|
"ApiBase": "https://02.g3stiona.com",
|
||||||
"AccessToken": "_yr.xVvPOllsyd1TYZRxUxg__c",
|
"AccessToken": "_yr.xVvPOllsyd1TYZRxUxg__c",
|
||||||
"ExternalProcedureId": "82722c9b-cecc-4299-8a7b-ce5abeb8170b",
|
"ProcedureName": "Procedimiento test 2",
|
||||||
"CircuitTemplateId": "bb997758-7436-46ab-9dc3-50dce2e02cfa",
|
"ExternalProcedureName": "",
|
||||||
"CircuitSignerStampHref": "https://02.g3stiona.com/rest/organ-stamps/3c6eaab4-7fcd-4b21-8676-bf8719be5d36",
|
"ExternalProcedureSiaCode": "3109963",
|
||||||
|
"ManagementUnitGroupCode": "700",
|
||||||
|
"CircuitTemplateName": "Firma automatizada",
|
||||||
|
"CircuitNewComplaintTemplateName": "CT-Nueva denuncia",
|
||||||
|
"CircuitUpdateSajTemplateName": "CT-Actualización denuncia SAJ",
|
||||||
|
"CircuitUpdateSdiTemplateName": "CT-Actualización denuncia SDI",
|
||||||
"CircuitSignerStampTitle": "oaaf-complaints-tramit",
|
"CircuitSignerStampTitle": "oaaf-complaints-tramit",
|
||||||
"CircuitRecipientGroupHref": "https://02.g3stiona.com/rest/groups/454fa4ec-8b82-4240-9419-113f45d4b004",
|
|
||||||
"CircuitVersion": "2",
|
"CircuitVersion": "2",
|
||||||
"UserLink": "https://02.g3stiona.com/rest/users/0c168833-8e27-4695-a301-b79924031f63",
|
"DocumentMetadataLanguage": "es",
|
||||||
"GroupLink": "https://02.g3stiona.com/rest/groups/6dbfc433-1eb6-4b9a-a533-bfebc652c101",
|
"DocumentMetadataType": "TD15",
|
||||||
"Location": "2.02.01"
|
"DocumentMetadataSubtype": "TD15_011"
|
||||||
},
|
},
|
||||||
"GlobalLeaks": {
|
"GlobalLeaks": {
|
||||||
"BaseUrl": "https://prebuzon.antifraudeandalucia.es",
|
"BaseUrl": "https://prebuzon.antifraudeandalucia.es",
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Globalization;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using PdfSharpCore.Drawing;
|
using PdfSharpCore.Drawing;
|
||||||
@@ -10,14 +11,30 @@ namespace GestionaDenuncias.Shared.Helpers
|
|||||||
{
|
{
|
||||||
public static class PdfHelper
|
public static class PdfHelper
|
||||||
{
|
{
|
||||||
|
private const double ReportTextMarginLeft = 95;
|
||||||
|
private const double ReportPdfSignatureGutter = 70;
|
||||||
|
private const double ReportPdfRightMargin = 20;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Fusiona varios ficheros (PDF, im<EFBFBD>genes, TXT) en un <EFBFBD>nico PDF.
|
/// Fusiona varios ficheros (PDF, imágenes, TXT) en un único PDF.
|
||||||
/// Los .txt se renderizan con m<EFBFBD>rgenes iguales, alineaci<EFBFBD>n a la izquierda y ajuste de l<EFBFBD>neas,
|
/// Los .txt se renderizan con márgenes iguales, alineación a la izquierda y ajuste de líneas,
|
||||||
/// preservando l<EFBFBD>neas en blanco.
|
/// preservando líneas en blanco.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="files">Secuencia de tuplas (FileName, ContentBytes)</param>
|
/// <param name="files">Secuencia de tuplas (FileName, ContentBytes)</param>
|
||||||
/// <returns>Bytes del PDF combinado</returns>
|
/// <returns>Bytes del PDF combinado</returns>
|
||||||
public static byte[] MergeFilesToPdf(IEnumerable<(string FileName, byte[] Content)> files)
|
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));
|
if (files == null) throw new ArgumentNullException(nameof(files));
|
||||||
|
|
||||||
@@ -33,8 +50,16 @@ namespace GestionaDenuncias.Shared.Helpers
|
|||||||
{
|
{
|
||||||
case ".pdf":
|
case ".pdf":
|
||||||
using (var src = PdfReader.Open(new MemoryStream(content), PdfDocumentOpenMode.Import))
|
using (var src = PdfReader.Open(new MemoryStream(content), PdfDocumentOpenMode.Import))
|
||||||
|
{
|
||||||
foreach (var page in src.Pages)
|
foreach (var page in src.Pages)
|
||||||
outputDoc.AddPage(page);
|
{
|
||||||
|
var importedPage = outputDoc.AddPage(page);
|
||||||
|
if (insetPdfPagesForSignature)
|
||||||
|
{
|
||||||
|
InsetPdfPageForSignature(importedPage);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case ".jpg":
|
case ".jpg":
|
||||||
@@ -52,12 +77,12 @@ namespace GestionaDenuncias.Shared.Helpers
|
|||||||
break;
|
break;
|
||||||
|
|
||||||
case ".txt":
|
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);
|
var text = Encoding.UTF8.GetString(content);
|
||||||
PdfPage pageTxt = outputDoc.AddPage();
|
PdfPage pageTxt = outputDoc.AddPage();
|
||||||
XGraphics gfxTxt = XGraphics.FromPdfPage(pageTxt);
|
XGraphics gfxTxt = XGraphics.FromPdfPage(pageTxt);
|
||||||
|
|
||||||
const double marginLeft = 40;
|
var marginLeft = textMarginLeft;
|
||||||
const double marginRight = 40;
|
const double marginRight = 40;
|
||||||
const double marginTop = 40;
|
const double marginTop = 40;
|
||||||
const double marginBottom = 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'))
|
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))
|
if (string.IsNullOrWhiteSpace(origLine))
|
||||||
{
|
{
|
||||||
y += lineHeight;
|
y += lineHeight;
|
||||||
@@ -99,7 +124,7 @@ namespace GestionaDenuncias.Shared.Helpers
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
// Dibujar la l<EFBFBD>nea acumulada
|
// Dibujar la línea acumulada
|
||||||
gfxTxt.DrawString(
|
gfxTxt.DrawString(
|
||||||
currentLine,
|
currentLine,
|
||||||
font,
|
font,
|
||||||
@@ -109,7 +134,7 @@ namespace GestionaDenuncias.Shared.Helpers
|
|||||||
y += lineHeight;
|
y += lineHeight;
|
||||||
currentLine = word;
|
currentLine = word;
|
||||||
|
|
||||||
// Paginaci<EFBFBD>n si se sale por abajo
|
// Paginación si se sale por abajo
|
||||||
if (y + lineHeight > pageHeight - marginBottom)
|
if (y + lineHeight > pageHeight - marginBottom)
|
||||||
{
|
{
|
||||||
gfxTxt.Dispose();
|
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))
|
if (!string.IsNullOrEmpty(currentLine))
|
||||||
{
|
{
|
||||||
gfxTxt.DrawString(
|
gfxTxt.DrawString(
|
||||||
@@ -145,7 +170,7 @@ namespace GestionaDenuncias.Shared.Helpers
|
|||||||
break;
|
break;
|
||||||
|
|
||||||
default:
|
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();
|
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"));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -36,12 +36,16 @@ public sealed record MarkReportImportedRequest(
|
|||||||
ReportDto Report,
|
ReportDto Report,
|
||||||
int? ComplaintId);
|
int? ComplaintId);
|
||||||
|
|
||||||
|
public sealed record MarkReportHandledInGestionaRequest(
|
||||||
|
string Username,
|
||||||
|
int DenunciaId,
|
||||||
|
DateTime UploadedAtUtc);
|
||||||
|
|
||||||
public sealed record TrackingImportPermissionRequest(
|
public sealed record TrackingImportPermissionRequest(
|
||||||
string Username,
|
string Username,
|
||||||
ReportDto Report);
|
ReportDto Report);
|
||||||
|
|
||||||
public sealed record GestionaCreateFileRequest(
|
public sealed record GestionaCreateFileRequest(
|
||||||
Guid ProcedureId,
|
|
||||||
string Subject,
|
string Subject,
|
||||||
string DocumentSeries,
|
string DocumentSeries,
|
||||||
string SiaCode);
|
string SiaCode);
|
||||||
@@ -53,16 +57,22 @@ public sealed record GestionaCreateFileResponse(
|
|||||||
public sealed record GestionaOpenFileRequest(
|
public sealed record GestionaOpenFileRequest(
|
||||||
string FileUrl,
|
string FileUrl,
|
||||||
string? FileOpenUrl,
|
string? FileOpenUrl,
|
||||||
Guid ManagementUnitGroupId,
|
string AssignedGroupCode,
|
||||||
Guid AssignedGroupId,
|
|
||||||
bool Confidential,
|
bool Confidential,
|
||||||
string FreeTitle,
|
string FreeTitle);
|
||||||
string SiaCode);
|
|
||||||
|
public sealed record GestionaAssignFileRequest(
|
||||||
|
string FileUrl,
|
||||||
|
string AssignedGroupCode);
|
||||||
|
|
||||||
public sealed record GestionaEnsureThirdRequest(
|
public sealed record GestionaEnsureThirdRequest(
|
||||||
string FileUrl,
|
string FileUrl,
|
||||||
ThirdPartyIdentityData ThirdParty);
|
ThirdPartyIdentityData ThirdParty);
|
||||||
|
|
||||||
|
public sealed record GestionaEnsureThirdResponse(
|
||||||
|
bool Ok,
|
||||||
|
IReadOnlyList<string> Warnings);
|
||||||
|
|
||||||
public sealed record GestionaCreateFolderRequest(
|
public sealed record GestionaCreateFolderRequest(
|
||||||
string FileUrl,
|
string FileUrl,
|
||||||
string FolderName);
|
string FolderName);
|
||||||
@@ -80,8 +90,9 @@ public sealed record GestionaUploadDocumentResponse(string DocumentUrl);
|
|||||||
|
|
||||||
public sealed record GestionaTramitarDocumentoRequest(
|
public sealed record GestionaTramitarDocumentoRequest(
|
||||||
string DocumentUrl,
|
string DocumentUrl,
|
||||||
string AssignedGroupHref,
|
string AssignedGroupCode,
|
||||||
int? ComplaintId);
|
int? ComplaintId,
|
||||||
|
bool IsUpdate = false);
|
||||||
|
|
||||||
public sealed record ManualPurgeRequest(string Date);
|
public sealed record ManualPurgeRequest(string Date);
|
||||||
|
|
||||||
@@ -91,6 +102,23 @@ public sealed record ManualPurgeResponse(
|
|||||||
int StatusCode,
|
int StatusCode,
|
||||||
string ResponseBody);
|
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 UpdateExternalUpdateCutoffRequest(string? Date);
|
||||||
|
|
||||||
|
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);
|
||||||
|
|||||||
@@ -77,6 +77,8 @@ public class DenunciasGestiona
|
|||||||
public string EstadoDenuncia { get; set; } = string.Empty;
|
public string EstadoDenuncia { get; set; } = string.Empty;
|
||||||
public string ArchivoElegido { get; set; } = string.Empty;
|
public string ArchivoElegido { get; set; } = string.Empty;
|
||||||
public DateTime FechaSubidaAGestiona { get; set; } = DateTime.MinValue;
|
public DateTime FechaSubidaAGestiona { get; set; } = DateTime.MinValue;
|
||||||
|
public string UltimaSubidaGestionaTipo { get; set; } = string.Empty;
|
||||||
|
public string UltimoGrupoAsignadoGestiona { get; set; } = string.Empty;
|
||||||
|
|
||||||
public bool EnGestiona { get; set; }
|
public bool EnGestiona { get; set; }
|
||||||
public bool EnRechazada { get; set; }
|
public bool EnRechazada { get; set; }
|
||||||
@@ -185,6 +187,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()
|
public IReadOnlyList<ReportFieldEntry> GetCamposFormulario()
|
||||||
{
|
{
|
||||||
if (string.IsNullOrWhiteSpace(CamposFormularioJson))
|
if (string.IsNullOrWhiteSpace(CamposFormularioJson))
|
||||||
|
|||||||
@@ -5,7 +5,10 @@ namespace GestionaDenuncias.Shared.Models
|
|||||||
public string FileUrl { get; set; } = "";
|
public string FileUrl { get; set; } = "";
|
||||||
public string? CodigoExpediente { get; set; } // name / code / etc.
|
public string? CodigoExpediente { get; set; } // name / code / etc.
|
||||||
public string? Asunto { get; set; } // subject / free_title
|
public string? Asunto { get; set; } // subject / free_title
|
||||||
|
public string? Procedimiento { get; set; }
|
||||||
public DateTimeOffset? FechaCreacion { get; set; }
|
public DateTimeOffset? FechaCreacion { get; set; }
|
||||||
|
public DateTimeOffset? FechaUltimaModificacion { get; set; }
|
||||||
|
public string? UltimaAuditoriaMensaje { get; set; }
|
||||||
public string? Estado { 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);
|
||||||
@@ -12,9 +12,18 @@ public sealed record ReportDto
|
|||||||
public string? ReminderDate { get; init; }
|
public string? ReminderDate { get; init; }
|
||||||
public string? AccessDate { get; init; }
|
public string? AccessDate { get; init; }
|
||||||
public string? LastAccess { get; init; }
|
public string? LastAccess { get; init; }
|
||||||
|
public string? WhistleblowerLastAccess { get; init; }
|
||||||
public string? Status { get; init; }
|
public string? Status { get; init; }
|
||||||
public bool Updated { get; init; }
|
public bool Updated { get; init; }
|
||||||
|
public bool? Accessible { get; init; }
|
||||||
public string? Label { 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 bool DownloadedByCurrentUser { get; init; }
|
public bool DownloadedByCurrentUser { get; init; }
|
||||||
public bool DownloadedByAnotherUser { get; init; }
|
public bool DownloadedByAnotherUser { get; init; }
|
||||||
public string? LastDownloadedByUsername { get; init; }
|
public string? LastDownloadedByUsername { get; init; }
|
||||||
|
|||||||
@@ -12,6 +12,9 @@ public sealed class ThirdPartyIdentityData
|
|||||||
public string BusinessName { get; set; } = string.Empty;
|
public string BusinessName { get; set; } = string.Empty;
|
||||||
public string Email { get; set; } = string.Empty;
|
public string Email { get; set; } = string.Empty;
|
||||||
public string CountryCode { 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 ThirdPartyAddressData? Address { get; set; }
|
||||||
|
|
||||||
public string DisplayName =>
|
public string DisplayName =>
|
||||||
@@ -53,6 +56,9 @@ public sealed class ThirdPartyIdentityData
|
|||||||
BusinessName = isAnonymous ? string.Empty : businessName,
|
BusinessName = isAnonymous ? string.Empty : businessName,
|
||||||
Email = (denuncia.Correo_Electronico ?? string.Empty).Trim(),
|
Email = (denuncia.Correo_Electronico ?? string.Empty).Trim(),
|
||||||
CountryCode = string.IsNullOrWhiteSpace(denuncia.Pais) ? denuncia.PaisOrigen : denuncia.Pais,
|
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)
|
Address = isAnonymous ? null : ThirdPartyAddressData.FromComplaint(denuncia)
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,9 +9,14 @@ public interface IDenunciaStore
|
|||||||
Task<List<DenunciasGestiona>> GetDenunciasByScopeAsync(DenunciaListScope scope, CancellationToken cancellationToken = default);
|
Task<List<DenunciasGestiona>> GetDenunciasByScopeAsync(DenunciaListScope scope, CancellationToken cancellationToken = default);
|
||||||
Task<List<FicherosDenuncias>> GetAllFicherosAsync(CancellationToken cancellationToken = default);
|
Task<List<FicherosDenuncias>> GetAllFicherosAsync(CancellationToken cancellationToken = default);
|
||||||
Task<List<FicherosDenuncias>> GetFicherosByDenunciaAsync(int denunciaId, 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<DenunciasGestiona?> GetDenunciaByIdAsync(int denunciaId, CancellationToken cancellationToken = default);
|
||||||
Task UpsertDenunciaAsync(DenunciasGestiona denuncia, CancellationToken cancellationToken = default);
|
Task UpsertDenunciaAsync(DenunciasGestiona denuncia, CancellationToken cancellationToken = default);
|
||||||
Task UpsertFicherosAsync(IEnumerable<FicherosDenuncias> ficheros, CancellationToken cancellationToken = default);
|
Task UpsertFicherosAsync(IEnumerable<FicherosDenuncias> ficheros, CancellationToken cancellationToken = default);
|
||||||
|
Task AddGestionaUploadHistoryAsync(
|
||||||
|
GestionaUploadHistoryCreateRequest request,
|
||||||
|
string username,
|
||||||
|
CancellationToken cancellationToken = default);
|
||||||
Task MarkFicherosAsUploadedAsync(
|
Task MarkFicherosAsUploadedAsync(
|
||||||
int denunciaId,
|
int denunciaId,
|
||||||
IEnumerable<string> fileNames,
|
IEnumerable<string> fileNames,
|
||||||
|
|||||||
@@ -15,6 +15,12 @@ public interface IInboxTrackingService
|
|||||||
int? complaintId,
|
int? complaintId,
|
||||||
CancellationToken cancellationToken = default);
|
CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
|
Task MarkReportHandledInGestionaAsync(
|
||||||
|
string username,
|
||||||
|
int denunciaId,
|
||||||
|
DateTime uploadedAtUtc,
|
||||||
|
CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
Task EnsureReportCanBeImportedByUserAsync(
|
Task EnsureReportCanBeImportedByUserAsync(
|
||||||
string username,
|
string username,
|
||||||
ReportDto report,
|
ReportDto report,
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
@inherits LayoutComponentBase
|
@inherits LayoutComponentBase
|
||||||
@implements IDisposable
|
@implements IDisposable
|
||||||
|
@using System.Globalization
|
||||||
@inject GestionaDenunciasAN.Models.UserState userState
|
@inject GestionaDenunciasAN.Models.UserState userState
|
||||||
@inject IHttpContextAccessor HttpContextAccessor
|
@inject IHttpContextAccessor HttpContextAccessor
|
||||||
@inject IJSRuntime JSRuntime
|
@inject IJSRuntime JSRuntime
|
||||||
@inject NavigationManager Navigation
|
@inject NavigationManager Navigation
|
||||||
@inject UiBusyService Busy
|
@inject UiBusyService Busy
|
||||||
|
@inject ApiDenunciasClient ApiDenuncias
|
||||||
|
|
||||||
<div class="app-shell">
|
<div class="app-shell">
|
||||||
<aside class="app-sidebar">
|
<aside class="app-sidebar">
|
||||||
@@ -20,11 +22,18 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="app-header__actions">
|
<div class="app-header__actions">
|
||||||
|
<div class="app-status-pills">
|
||||||
<div class="app-session-pill">
|
<div class="app-session-pill">
|
||||||
<span class="app-session-pill__dot"></span>
|
<span class="app-session-pill__dot"></span>
|
||||||
Sesion interna activa
|
Sesion interna activa
|
||||||
</div>
|
</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">
|
<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="bi bi-person-circle app-user-chip__icon" aria-hidden="true"></span>
|
||||||
<span class="app-user-chip__text">
|
<span class="app-user-chip__text">
|
||||||
@@ -53,6 +62,9 @@
|
|||||||
private string CurrentPageTitle { get; set; } = "Portal de gestion";
|
private string CurrentPageTitle { get; set; } = "Portal de gestion";
|
||||||
private string CurrentPageDescription { get; set; } =
|
private string CurrentPageDescription { get; set; } =
|
||||||
"Entrada, revision y tramitacion coordinada de denuncias y actualizaciones.";
|
"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 string DisplayUsername =>
|
private string DisplayUsername =>
|
||||||
string.IsNullOrWhiteSpace(userState?.NombreUsu)
|
string.IsNullOrWhiteSpace(userState?.NombreUsu)
|
||||||
@@ -65,6 +77,11 @@
|
|||||||
RefreshLayoutState();
|
RefreshLayoutState();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected override async Task OnInitializedAsync()
|
||||||
|
{
|
||||||
|
await LoadEncryptionKeyStatusAsync();
|
||||||
|
}
|
||||||
|
|
||||||
protected override void OnParametersSet()
|
protected override void OnParametersSet()
|
||||||
{
|
{
|
||||||
RefreshLayoutState();
|
RefreshLayoutState();
|
||||||
@@ -78,7 +95,75 @@
|
|||||||
private void HandleLocationChanged(object? sender, LocationChangedEventArgs args)
|
private void HandleLocationChanged(object? sender, LocationChangedEventArgs args)
|
||||||
{
|
{
|
||||||
RefreshLayoutState();
|
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 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()
|
private void RefreshLayoutState()
|
||||||
@@ -98,7 +183,7 @@
|
|||||||
|
|
||||||
return path.ToLowerInvariant() switch
|
return path.ToLowerInvariant() switch
|
||||||
{
|
{
|
||||||
"" or "gestionzip" => (
|
"" or "entrada" => (
|
||||||
"Entrada de denuncias",
|
"Entrada de denuncias",
|
||||||
"Importa lo nuevo desde GlobalLeaks, revisa el seguimiento por usuario y decide si habra expediente nuevo o actualizacion."),
|
"Importa lo nuevo desde GlobalLeaks, revisa el seguimiento por usuario y decide si habra expediente nuevo o actualizacion."),
|
||||||
"pendientes" => (
|
"pendientes" => (
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
@using System.Security.Claims
|
||||||
|
|
||||||
<div class="nav-shell">
|
<div class="nav-shell">
|
||||||
<div class="nav-brand">
|
<div class="nav-brand">
|
||||||
<img class="nav-brand__logo"
|
<img class="nav-brand__logo"
|
||||||
@@ -14,7 +16,7 @@
|
|||||||
<div class="nav-section">
|
<div class="nav-section">
|
||||||
<span class="nav-section__label">Operativa diaria</span>
|
<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__icon bi bi-box-seam" aria-hidden="true"></span>
|
||||||
<span class="menu-link__content">
|
<span class="menu-link__content">
|
||||||
<span class="menu-link__title">Entrada</span>
|
<span class="menu-link__title">Entrada</span>
|
||||||
@@ -66,6 +68,10 @@
|
|||||||
</span>
|
</span>
|
||||||
</NavLink>
|
</NavLink>
|
||||||
|
|
||||||
|
<AuthorizeView>
|
||||||
|
<Authorized Context="authContext">
|
||||||
|
@if (IsConfigurationUser(authContext.User))
|
||||||
|
{
|
||||||
<NavLink class="menu-link" href="/Configuracion" Match="NavLinkMatch.All">
|
<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__icon bi bi-gear" aria-hidden="true"></span>
|
||||||
<span class="menu-link__content">
|
<span class="menu-link__content">
|
||||||
@@ -73,6 +79,9 @@
|
|||||||
<span class="menu-link__meta">Fecha de corte y purga manual</span>
|
<span class="menu-link__meta">Fecha de corte y purga manual</span>
|
||||||
</span>
|
</span>
|
||||||
</NavLink>
|
</NavLink>
|
||||||
|
}
|
||||||
|
</Authorized>
|
||||||
|
</AuthorizeView>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="nav-section nav-section--footer">
|
<div class="nav-section nav-section--footer">
|
||||||
@@ -88,3 +97,19 @@
|
|||||||
</div>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
</div>
|
</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
|
@rendermode InteractiveServer
|
||||||
@attribute [Authorize]
|
@attribute [Authorize]
|
||||||
@using GestionaDenuncias.Shared.Models
|
@using GestionaDenuncias.Shared.Models
|
||||||
@@ -58,7 +58,7 @@
|
|||||||
margin: 1rem 0 0.5rem;
|
margin: 1rem 0 0.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Tarjetas de actualizaci<EFBFBD>n (azules) */
|
/* Tarjetas de actualización (azules) */
|
||||||
.collapse-card.update-card {
|
.collapse-card.update-card {
|
||||||
background-color: #e3f2fd;
|
background-color: #e3f2fd;
|
||||||
}
|
}
|
||||||
@@ -73,7 +73,7 @@
|
|||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* === Est<EFBFBD>tica de modal igual que en Pendientes === */
|
/* === Estética de modal igual que en Pendientes === */
|
||||||
|
|
||||||
.custom-modal {
|
.custom-modal {
|
||||||
background: rgba(0, 0, 0, 0.5);
|
background: rgba(0, 0, 0, 0.5);
|
||||||
@@ -162,7 +162,7 @@ else
|
|||||||
data-bs-target="#@collapseId"
|
data-bs-target="#@collapseId"
|
||||||
aria-expanded="false"
|
aria-expanded="false"
|
||||||
aria-controls="@collapseId">
|
aria-controls="@collapseId">
|
||||||
<h5 class="mb-0">Denuncia ID: @denuncia.Id_Denuncia (Actualizaci<EFBFBD>n)</h5>
|
<h5 class="mb-0">Denuncia ID: @denuncia.Id_Denuncia (Actualización)</h5>
|
||||||
|
|
||||||
<div class="d-flex align-items-center">
|
<div class="d-flex align-items-center">
|
||||||
<div class="text-muted small me-3">
|
<div class="text-muted small me-3">
|
||||||
@@ -176,19 +176,11 @@ else
|
|||||||
}
|
}
|
||||||
</div>
|
</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"
|
<button type="button"
|
||||||
class="btn btn-success btn-sm"
|
class="btn btn-success btn-sm"
|
||||||
@onclick:stopPropagation="true"
|
@onclick:stopPropagation="true"
|
||||||
@onclick="() => OpenEnviarAGestionaModal(denuncia)">
|
@onclick="() => OpenEnviarAGestionaModal(denuncia)">
|
||||||
Configurar subida
|
Configurar actualizacion expediente
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -204,7 +196,7 @@ else
|
|||||||
}
|
}
|
||||||
@if (!string.IsNullOrWhiteSpace(denuncia.ExpedienteGestionaMostrable))
|
@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>
|
<dd class="col-sm-9">@denuncia.ExpedienteGestionaMostrable</dd>
|
||||||
}
|
}
|
||||||
@if (!string.IsNullOrWhiteSpace(denuncia.Etiqueta))
|
@if (!string.IsNullOrWhiteSpace(denuncia.Etiqueta))
|
||||||
@@ -245,7 +237,7 @@ else
|
|||||||
}
|
}
|
||||||
@if (!string.IsNullOrWhiteSpace(denuncia.RazonSocialResuelta))
|
@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>
|
<dd class="col-sm-9">@denuncia.RazonSocialResuelta</dd>
|
||||||
}
|
}
|
||||||
@if (!string.IsNullOrWhiteSpace(denuncia.NombreResuelto))
|
@if (!string.IsNullOrWhiteSpace(denuncia.NombreResuelto))
|
||||||
@@ -255,12 +247,12 @@ else
|
|||||||
}
|
}
|
||||||
@if (!string.IsNullOrWhiteSpace(denuncia.PrimerApellidoResuelto))
|
@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>
|
<dd class="col-sm-9">@denuncia.PrimerApellidoResuelto</dd>
|
||||||
}
|
}
|
||||||
@if (!string.IsNullOrWhiteSpace(denuncia.SegundoApellidoResuelto))
|
@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>
|
<dd class="col-sm-9">@denuncia.SegundoApellidoResuelto</dd>
|
||||||
}
|
}
|
||||||
@if (!string.IsNullOrWhiteSpace(denuncia.ApellidosResueltos))
|
@if (!string.IsNullOrWhiteSpace(denuncia.ApellidosResueltos))
|
||||||
@@ -280,14 +272,14 @@ else
|
|||||||
<dl class="row">
|
<dl class="row">
|
||||||
<dt class="col-sm-3">Asunto</dt>
|
<dt class="col-sm-3">Asunto</dt>
|
||||||
<dd class="col-sm-9">@denuncia.Asunto</dd>
|
<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>
|
<dd class="col-sm-9">@denuncia.A_Quien_Denuncia</dd>
|
||||||
@if (!string.IsNullOrWhiteSpace(denuncia.DenunciadoDetalle))
|
@if (!string.IsNullOrWhiteSpace(denuncia.DenunciadoDetalle))
|
||||||
{
|
{
|
||||||
<dt class="col-sm-3">Detalle denunciado</dt>
|
<dt class="col-sm-3">Detalle denunciado</dt>
|
||||||
<dd class="col-sm-9">@denuncia.DenunciadoDetalle</dd>
|
<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>
|
<dd class="col-sm-9">@denuncia.Descripcion_Denuncia</dd>
|
||||||
@if (!string.IsNullOrWhiteSpace(denuncia.OrganismoDenunciado))
|
@if (!string.IsNullOrWhiteSpace(denuncia.OrganismoDenunciado))
|
||||||
{
|
{
|
||||||
@@ -296,7 +288,7 @@ else
|
|||||||
}
|
}
|
||||||
@if (!string.IsNullOrWhiteSpace(denuncia.SolicitaProteccion))
|
@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>
|
<dd class="col-sm-9">@denuncia.SolicitaProteccion</dd>
|
||||||
}
|
}
|
||||||
@if (!string.IsNullOrWhiteSpace(denuncia.MedidasProteccionSolicitadas))
|
@if (!string.IsNullOrWhiteSpace(denuncia.MedidasProteccionSolicitadas))
|
||||||
@@ -313,7 +305,7 @@ else
|
|||||||
}
|
}
|
||||||
@if (!string.IsNullOrWhiteSpace(denuncia.AutorizaRemision))
|
@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>
|
<dd class="col-sm-9">@denuncia.AutorizaRemision</dd>
|
||||||
}
|
}
|
||||||
</dl>
|
</dl>
|
||||||
@@ -324,14 +316,14 @@ else
|
|||||||
@if (camposFormulario.Count > 0)
|
@if (camposFormulario.Count > 0)
|
||||||
{
|
{
|
||||||
<h5 class="section-heading">Formulario Original</h5>
|
<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>
|
<h6 class="mt-3">@grupoCampos.Key</h6>
|
||||||
<dl class="row">
|
<dl class="row">
|
||||||
@foreach (var campo in grupoCampos)
|
@foreach (var campo in grupoCampos)
|
||||||
{
|
{
|
||||||
<dt class="col-sm-4">@campo.Label</dt>
|
<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>
|
</dl>
|
||||||
}
|
}
|
||||||
@@ -347,7 +339,7 @@ else
|
|||||||
<th>Nombre</th>
|
<th>Nombre</th>
|
||||||
<th>Fecha</th>
|
<th>Fecha</th>
|
||||||
<th>Motivo</th>
|
<th>Motivo</th>
|
||||||
<th>Tama<EFBFBD>o (bytes)</th>
|
<th>Tamaño (bytes)</th>
|
||||||
<th>Ver</th>
|
<th>Ver</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
@@ -396,7 +388,7 @@ else
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
<span class="text-muted"><EFBFBD></span>
|
<span class="text-muted">—</span>
|
||||||
}
|
}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -447,19 +439,19 @@ else
|
|||||||
{
|
{
|
||||||
<div class="alert alert-info d-flex align-items-center" role="alert">
|
<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>
|
<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>
|
</div>
|
||||||
}
|
}
|
||||||
else if (autoSearchTried && !string.IsNullOrWhiteSpace(autoFoundFileUrl))
|
else if (autoSearchTried && !string.IsNullOrWhiteSpace(autoFoundFileUrl))
|
||||||
{
|
{
|
||||||
<div class="alert alert-success" role="alert">
|
<div class="alert alert-success" role="alert">
|
||||||
<div class="fw-semibold">Expediente detectado en Gestiona.</div>
|
<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="text-muted small">@autoFoundFileUrl</div>
|
||||||
<div class="form-check mt-2">
|
<div class="form-check mt-2">
|
||||||
<input class="form-check-input" type="checkbox" id="chkUsarDetectado" @bind="useAutoFoundExpediente" />
|
<input class="form-check-input" type="checkbox" id="chkUsarDetectado" @bind="useAutoFoundExpediente" />
|
||||||
<label class="form-check-label" for="chkUsarDetectado">
|
<label class="form-check-label" for="chkUsarDetectado">
|
||||||
A<EFBFBD>adir documentos a este expediente.
|
Añadir documentos a este expediente.
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -468,12 +460,7 @@ else
|
|||||||
{
|
{
|
||||||
<div class="alert alert-warning" role="alert">
|
<div class="alert alert-warning" role="alert">
|
||||||
<div class="fw-semibold">No se ha detectado expediente en Gestiona por asunto.</div>
|
<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="small">Puede que esto no sea una actualizació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>
|
</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -491,17 +478,25 @@ else
|
|||||||
</div>
|
</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">
|
<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>
|
</div>
|
||||||
|
|
||||||
<h6 class="modal-section-heading">Nombre de los documentos</h6>
|
<h6 class="modal-section-heading">Nombre de los documentos</h6>
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<input type="text" class="form-control" @bind="nombreDocumentos"
|
<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">
|
<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>
|
</small>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -509,7 +504,7 @@ else
|
|||||||
<div class="form-check">
|
<div class="form-check">
|
||||||
<input class="form-check-input" type="radio" name="uploadMode" id="modoMerge"
|
<input class="form-check-input" type="radio" name="uploadMode" id="modoMerge"
|
||||||
checked='@(uploadMode == "merge")' @onclick='() => uploadMode = "merge"' />
|
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>
|
||||||
<div class="form-check">
|
<div class="form-check">
|
||||||
<input class="form-check-input" type="radio" name="uploadMode" id="modoIndividual"
|
<input class="form-check-input" type="radio" name="uploadMode" id="modoIndividual"
|
||||||
@@ -522,18 +517,13 @@ else
|
|||||||
<input class="form-check-input" type="radio" name="selectedGroup" id="grupo600"
|
<input class="form-check-input" type="radio" name="selectedGroup" id="grupo600"
|
||||||
checked='@(selectedGroup == "600")' @onclick='() => selectedGroup = "600"' />
|
checked='@(selectedGroup == "600")' @onclick='() => selectedGroup = "600"' />
|
||||||
<label class="form-check-label" for="grupo600">
|
<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>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-check">
|
<div class="form-check">
|
||||||
<input class="form-check-input" type="radio" name="selectedGroup" id="grupo510"
|
<input class="form-check-input" type="radio" name="selectedGroup" id="grupo510"
|
||||||
checked='@(selectedGroup == "510")' @onclick='() => selectedGroup = "510"' />
|
checked='@(selectedGroup == "510")' @onclick='() => selectedGroup = "510"' />
|
||||||
<label class="form-check-label" for="grupo510">510. SDI <EFBFBD> Investigaci<EFBFBD>n Entradas</label>
|
<label class="form-check-label" for="grupo510">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>
|
</div>
|
||||||
|
|
||||||
@{
|
@{
|
||||||
@@ -542,13 +532,13 @@ else
|
|||||||
<h6 class="modal-section-heading mt-3">Tercero (denunciante)</h6>
|
<h6 class="modal-section-heading mt-3">Tercero (denunciante)</h6>
|
||||||
|
|
||||||
<div class="alert alert-light border mb-3">
|
<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>
|
</div>
|
||||||
|
|
||||||
@if (modalThirdParty.IsAnonymous)
|
@if (modalThirdParty.IsAnonymous)
|
||||||
{
|
{
|
||||||
<div class="alert alert-warning mb-3">
|
<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>
|
</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -577,7 +567,7 @@ else
|
|||||||
{
|
{
|
||||||
<div class="row g-2">
|
<div class="row g-2">
|
||||||
<div class="col-12 mb-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 />
|
<input class="form-control" value="@GetReadOnlyValue(selectedDenuncias.RazonSocialResuelta)" readonly />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -590,11 +580,11 @@ else
|
|||||||
<input class="form-control" value="@GetReadOnlyValue(selectedDenuncias.NombreResuelto)" readonly />
|
<input class="form-control" value="@GetReadOnlyValue(selectedDenuncias.NombreResuelto)" readonly />
|
||||||
</div>
|
</div>
|
||||||
<div class="col-4 mb-2">
|
<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 />
|
<input class="form-control" value="@GetReadOnlyValue(selectedDenuncias.PrimerApellidoResuelto)" readonly />
|
||||||
</div>
|
</div>
|
||||||
<div class="col-4 mb-2">
|
<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 />
|
<input class="form-control" value="@GetReadOnlyValue(selectedDenuncias.SegundoApellidoResuelto)" readonly />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -604,14 +594,14 @@ else
|
|||||||
{
|
{
|
||||||
<div class="row g-2">
|
<div class="row g-2">
|
||||||
<div class="col-12 mb-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>
|
<textarea class="form-control" rows="2" readonly>@BuildPostalAddressSummary(selectedDenuncias)</textarea>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
<small class="text-muted">
|
<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>
|
</small>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -639,14 +629,14 @@ else
|
|||||||
// --- Modal / estado ---
|
// --- Modal / estado ---
|
||||||
private bool showModal = false;
|
private bool showModal = false;
|
||||||
private bool isUploading = false;
|
private bool isUploading = false;
|
||||||
private string uploadMode = "merge";
|
private string uploadMode = "individual";
|
||||||
private string selectedGroup = "600";
|
private string selectedGroup = "600";
|
||||||
private string nuevoAsunto = "";
|
private string nuevoAsunto = "";
|
||||||
private string nombreDocumentos = "";
|
private string nombreDocumentos = "";
|
||||||
private DenunciasGestiona? selectedDenuncias;
|
private DenunciasGestiona? selectedDenuncias;
|
||||||
private ThirdPartyIdentityData? selectedThirdParty;
|
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 autoSearchLoading = false;
|
||||||
private bool autoSearchTried = false;
|
private bool autoSearchTried = false;
|
||||||
private string? autoFoundFileUrl = null;
|
private string? autoFoundFileUrl = null;
|
||||||
@@ -668,19 +658,15 @@ else
|
|||||||
var config = await ApiDenuncias.GetAppConfigurationAsync();
|
var config = await ApiDenuncias.GetAppConfigurationAsync();
|
||||||
externalUpdateCutoffDate = ParseConfiguredCutoffDate(config.ExternalUpdateCutoffDate);
|
externalUpdateCutoffDate = ParseConfiguredCutoffDate(config.ExternalUpdateCutoffDate);
|
||||||
var todas = await CargarDenunciasJsonAsync();
|
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
|
actualizaciones = todas
|
||||||
.Where(d => d.EsActualizacion)
|
.Where(d => d.EsActualizacion)
|
||||||
.OrderByDescending(d => d.FechaSubidaAGestiona != DateTime.MinValue ? d.FechaSubidaAGestiona : d.Fecha)
|
.OrderByDescending(d => d.FechaSubidaAGestiona != DateTime.MinValue ? d.FechaSubidaAGestiona : d.Fecha)
|
||||||
.ToList();
|
.ToList();
|
||||||
|
|
||||||
ficherosAdjuntos = await CargarFicherosPorDenunciaAsync(actualizaciones);
|
ficherosAdjuntos = await CargarFicherosPorDenunciaAsync(actualizaciones);
|
||||||
|
actualizaciones = actualizaciones
|
||||||
|
.Where(d => ficherosAdjuntos.ContainsKey(d.Id_Denuncia))
|
||||||
|
.ToList();
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
@@ -715,12 +701,13 @@ else
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OpenEnviarAGestionaModal(DenunciasGestiona d)
|
private async Task OpenEnviarAGestionaModal(DenunciasGestiona d)
|
||||||
{
|
{
|
||||||
selectedDenuncias = d;
|
selectedDenuncias = d;
|
||||||
nuevoAsunto = string.IsNullOrWhiteSpace(d.NombreDenuncia) ? $"Denuncia-{d.Id_Denuncia}-CD" : d.NombreDenuncia;
|
nuevoAsunto = string.IsNullOrWhiteSpace(d.NombreDenuncia) ? $"Denuncia-{d.Id_Denuncia}-CD" : d.NombreDenuncia;
|
||||||
nombreDocumentos = "";
|
nombreDocumentos = "";
|
||||||
selectedThirdParty = ThirdPartyIdentityData.FromComplaint(d);
|
selectedThirdParty = ThirdPartyIdentityData.FromComplaint(d);
|
||||||
|
selectedGroup = NormalizeUpdateGroup(selectedGroup);
|
||||||
operationError = string.Empty;
|
operationError = string.Empty;
|
||||||
operationNotice = string.Empty;
|
operationNotice = string.Empty;
|
||||||
|
|
||||||
@@ -729,6 +716,12 @@ else
|
|||||||
useAutoFoundExpediente = true;
|
useAutoFoundExpediente = true;
|
||||||
autoSearchTried = false;
|
autoSearchTried = false;
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(d.Expediente_Gestiona) &&
|
||||||
|
string.IsNullOrWhiteSpace(d.CodigoExpedienteGestiona))
|
||||||
|
{
|
||||||
|
await SincronizarExpedienteGestionaAsync(d, d.Expediente_Gestiona);
|
||||||
|
}
|
||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(d.Expediente_Gestiona))
|
if (string.IsNullOrWhiteSpace(d.Expediente_Gestiona))
|
||||||
{
|
{
|
||||||
autoSearchLoading = true;
|
autoSearchLoading = true;
|
||||||
@@ -737,7 +730,7 @@ else
|
|||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
// B<EFBFBD>squeda autom<EFBFBD>tica desactivada temporalmente
|
// Búsqueda automática desactivada temporalmente
|
||||||
}
|
}
|
||||||
catch
|
catch
|
||||||
{
|
{
|
||||||
@@ -799,48 +792,20 @@ else
|
|||||||
return new string(clean.Where(ch => ch <= 127).ToArray());
|
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)
|
private async Task ActualizarDenunciaAsync(DenunciasGestiona d)
|
||||||
{
|
{
|
||||||
await DenunciaStore.UpsertDenunciaAsync(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()
|
private async Task ConfirmarEnvio()
|
||||||
{
|
{
|
||||||
if (selectedDenuncias == null) return;
|
if (selectedDenuncias == null) return;
|
||||||
@@ -857,6 +822,7 @@ else
|
|||||||
isUploading = true;
|
isUploading = true;
|
||||||
operationError = string.Empty;
|
operationError = string.Empty;
|
||||||
operationNotice = string.Empty;
|
operationNotice = string.Empty;
|
||||||
|
selectedGroup = NormalizeUpdateGroup(selectedGroup);
|
||||||
using var busy = Busy.Show(
|
using var busy = Busy.Show(
|
||||||
"Enviando actualizacion",
|
"Enviando actualizacion",
|
||||||
"Preparando expediente, carpeta de actualizacion y documentos.");
|
"Preparando expediente, carpeta de actualizacion y documentos.");
|
||||||
@@ -889,12 +855,13 @@ else
|
|||||||
if (!todos.Any())
|
if (!todos.Any())
|
||||||
{
|
{
|
||||||
operationError = ficherosVacios.Count == 0
|
operationError = ficherosVacios.Count == 0
|
||||||
? $"La denuncia #{selectedDenuncias.Id_Denuncia} no tiene ficheros pendientes para subir como actualizaci<EFBFBD>n."
|
? $"La denuncia #{selectedDenuncias.Id_Denuncia} no tiene ficheros pendientes para subir como actualizació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} solo tiene ficheros vacíos y no se ha subido nada. Ficheros omitidos: {string.Join(", ", ficherosVacios)}.";
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2) Determinar expediente destino
|
// 2) Determinar expediente destino
|
||||||
|
var expedienteCreadoEnGestiona = false;
|
||||||
string fileUrl;
|
string fileUrl;
|
||||||
if (selectedDenuncias.EnGestiona && !string.IsNullOrWhiteSpace(selectedDenuncias.Expediente_Gestiona))
|
if (selectedDenuncias.EnGestiona && !string.IsNullOrWhiteSpace(selectedDenuncias.Expediente_Gestiona))
|
||||||
{
|
{
|
||||||
@@ -910,7 +877,6 @@ else
|
|||||||
{
|
{
|
||||||
Busy.Update(message: "Creando expediente nuevo en Gestiona.", detail: "Paso 2 de 8");
|
Busy.Update(message: "Creando expediente nuevo en Gestiona.", detail: "Paso 2 de 8");
|
||||||
var createdFile = await ApiDenuncias.CreateGestionaFileAsync(
|
var createdFile = await ApiDenuncias.CreateGestionaFileAsync(
|
||||||
selectedDenuncias.ProcedureId,
|
|
||||||
nuevoAsunto,
|
nuevoAsunto,
|
||||||
"RQ2ZLC - Expediente de Denuncias",
|
"RQ2ZLC - Expediente de Denuncias",
|
||||||
"3109963"
|
"3109963"
|
||||||
@@ -920,33 +886,29 @@ else
|
|||||||
await ApiDenuncias.OpenGestionaFileAsync(
|
await ApiDenuncias.OpenGestionaFileAsync(
|
||||||
fileUrl,
|
fileUrl,
|
||||||
createdFile.FileOpenUrl,
|
createdFile.FileOpenUrl,
|
||||||
managementUnitGroupId: Guid.Parse("a4ad4dfb-70dc-4219-8ee3-4dcc939f0955"),
|
assignedGroupCode: selectedGroup,
|
||||||
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}")
|
|
||||||
},
|
|
||||||
confidential: selectedDenuncias.Confidencial,
|
confidential: selectedDenuncias.Confidencial,
|
||||||
freeTitle: nuevoAsunto,
|
freeTitle: nuevoAsunto
|
||||||
siaCode: "3109963"
|
|
||||||
);
|
);
|
||||||
selectedDenuncias.Expediente_Gestiona = fileUrl;
|
selectedDenuncias.Expediente_Gestiona = fileUrl;
|
||||||
selectedDenuncias.EnGestiona = true;
|
selectedDenuncias.EnGestiona = true;
|
||||||
|
expedienteCreadoEnGestiona = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
Busy.Update(message: "Guardando referencia local del expediente.", detail: "Paso 3 de 8");
|
Busy.Update(message: "Guardando referencia local del expediente.", detail: "Paso 3 de 8");
|
||||||
selectedDenuncias.Expediente_Gestiona = fileUrl;
|
selectedDenuncias.Expediente_Gestiona = fileUrl;
|
||||||
selectedDenuncias.EnGestiona = true;
|
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);
|
var thirdParty = selectedThirdParty ?? ThirdPartyIdentityData.FromComplaint(selectedDenuncias);
|
||||||
|
|
||||||
Busy.Update(message: "Resolviendo tercero y enlazandolo al expediente.", detail: "Paso 4 de 8");
|
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 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");
|
Busy.Update(message: "Creando carpeta de actualizacion en Gestiona.", detail: "Paso 5 de 8");
|
||||||
var carpetaActualizacionGestiona = await ApiDenuncias.CreateGestionaFolderAsync(fileUrl, carpetaActualizacion);
|
var carpetaActualizacionGestiona = await ApiDenuncias.CreateGestionaFolderAsync(fileUrl, carpetaActualizacion);
|
||||||
var documentsTargetUrl = carpetaActualizacionGestiona.DocumentsTargetUrl;
|
var documentsTargetUrl = carpetaActualizacionGestiona.DocumentsTargetUrl;
|
||||||
@@ -959,8 +921,8 @@ else
|
|||||||
|
|
||||||
if (!string.IsNullOrWhiteSpace(report.FileName))
|
if (!string.IsNullOrWhiteSpace(report.FileName))
|
||||||
{
|
{
|
||||||
Busy.Update(message: "Preparando y subiendo el report para firma.", detail: "Paso 6 de 8");
|
Busy.Update(message: "Preparando y subiendo el documento principal.", detail: "Paso 6 de 8");
|
||||||
var reportPdfBytes = PdfHelper.MergeFilesToPdf(
|
var reportPdfBytes = PdfHelper.MergeReportToPdf(
|
||||||
new (string FileName, byte[] Content)[] { (report.FileName, report.Content) });
|
new (string FileName, byte[] Content)[] { (report.FileName, report.Content) });
|
||||||
var reportFinalName = FixFileName("Denuncia.pdf");
|
var reportFinalName = FixFileName("Denuncia.pdf");
|
||||||
documentoParaTramitar = await ApiDenuncias.UploadGestionaDocumentAsync(
|
documentoParaTramitar = await ApiDenuncias.UploadGestionaDocumentAsync(
|
||||||
@@ -979,7 +941,7 @@ else
|
|||||||
{
|
{
|
||||||
Busy.Update(message: "Uniendo adjuntos nuevos en un unico PDF y subiendolo.", detail: "Paso 7 de 8");
|
Busy.Update(message: "Uniendo adjuntos nuevos en un unico PDF y subiendolo.", detail: "Paso 7 de 8");
|
||||||
var pdfBytes = PdfHelper.MergeFilesToPdf(adjuntos);
|
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);
|
var docUrl = await ApiDenuncias.UploadGestionaDocumentAsync(documentsTargetUrl, pdfBytes, pdfName);
|
||||||
if (string.IsNullOrWhiteSpace(documentoParaTramitar))
|
if (string.IsNullOrWhiteSpace(documentoParaTramitar))
|
||||||
{
|
{
|
||||||
@@ -1040,11 +1002,13 @@ else
|
|||||||
|
|
||||||
if (!string.IsNullOrWhiteSpace(documentoParaTramitar))
|
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(
|
await ApiDenuncias.TramitarGestionaDocumentAsync(
|
||||||
documentoParaTramitar,
|
documentoParaTramitar,
|
||||||
GetAssignedGroupLinkBySelectedGroup(),
|
selectedGroup,
|
||||||
selectedDenuncias.Id_Denuncia);
|
selectedDenuncias.Id_Denuncia,
|
||||||
|
isUpdate: true);
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach (var orig in nombresOriginalesSubidos)
|
foreach (var orig in nombresOriginalesSubidos)
|
||||||
@@ -1066,7 +1030,16 @@ else
|
|||||||
selectedDenuncias.EsActualizacion = false;
|
selectedDenuncias.EsActualizacion = false;
|
||||||
selectedDenuncias.NombreDenuncia = nuevoAsunto;
|
selectedDenuncias.NombreDenuncia = nuevoAsunto;
|
||||||
selectedDenuncias.FechaSubidaAGestiona = ahoraUtc;
|
selectedDenuncias.FechaSubidaAGestiona = ahoraUtc;
|
||||||
|
selectedDenuncias.UltimaSubidaGestionaTipo = "Actualización";
|
||||||
|
selectedDenuncias.UltimoGrupoAsignadoGestiona = GetGestionaGroupDisplay(selectedGroup);
|
||||||
|
await SincronizarExpedienteGestionaAsync(selectedDenuncias, fileUrl);
|
||||||
await ActualizarDenunciaAsync(selectedDenuncias);
|
await ActualizarDenunciaAsync(selectedDenuncias);
|
||||||
|
var historialAviso = await RegistrarHistorialGestionaAsync(
|
||||||
|
selectedDenuncias,
|
||||||
|
"Actualización",
|
||||||
|
selectedGroup,
|
||||||
|
ahoraUtc,
|
||||||
|
string.Join("; ", nombresFinalesSubidos));
|
||||||
|
|
||||||
var denunciaProcesadaId = selectedDenuncias.Id_Denuncia;
|
var denunciaProcesadaId = selectedDenuncias.Id_Denuncia;
|
||||||
actualizaciones.RemoveAll(x => x.Id_Denuncia == denunciaProcesadaId);
|
actualizaciones.RemoveAll(x => x.Id_Denuncia == denunciaProcesadaId);
|
||||||
@@ -1076,22 +1049,35 @@ else
|
|||||||
var avisos = new List<string>();
|
var avisos = new List<string>();
|
||||||
if (ficherosVacios.Count > 0)
|
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)
|
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();
|
StateHasChanged();
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
Console.Error.WriteLine($"Error al confirmar env<EFBFBD>o (Actualizaciones): {ex}");
|
Console.Error.WriteLine($"Error al confirmar envío (Actualizaciones): {ex}");
|
||||||
operationError = $"No se ha podido completar la actualizaci<EFBFBD>n #{selectedDenuncias?.Id_Denuncia}: {ex.Message}";
|
operationError = $"No se ha podido completar la actualización #{selectedDenuncias?.Id_Denuncia}: {ex.Message}";
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
@@ -1186,7 +1172,7 @@ else
|
|||||||
}
|
}
|
||||||
|
|
||||||
private static string GetReadOnlyValue(string? value) =>
|
private static string GetReadOnlyValue(string? value) =>
|
||||||
string.IsNullOrWhiteSpace(value) ? "<EFBFBD>" : value;
|
string.IsNullOrWhiteSpace(value) ? "—" : value;
|
||||||
|
|
||||||
private static bool IsExternalGestionaUpdate(DenunciasGestiona denuncia)
|
private static bool IsExternalGestionaUpdate(DenunciasGestiona denuncia)
|
||||||
{
|
{
|
||||||
@@ -1264,7 +1250,7 @@ else
|
|||||||
private static List<FicherosDenuncias> GetPendingUpdateFiles(List<FicherosDenuncias> files, DateOnly? externalUpdateCutoffDate = null)
|
private static List<FicherosDenuncias> GetPendingUpdateFiles(List<FicherosDenuncias> files, DateOnly? externalUpdateCutoffDate = null)
|
||||||
{
|
{
|
||||||
var uploadedHashes = files
|
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)
|
.Select(file => file.ContentSha256)
|
||||||
.ToHashSet(StringComparer.OrdinalIgnoreCase);
|
.ToHashSet(StringComparer.OrdinalIgnoreCase);
|
||||||
|
|
||||||
@@ -1275,10 +1261,13 @@ else
|
|||||||
.Where(file => file.EsReport)
|
.Where(file => file.EsReport)
|
||||||
.OrderByDescending(file => file.Fecha)
|
.OrderByDescending(file => file.Fecha)
|
||||||
.FirstOrDefault();
|
.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);
|
pending.Add(report);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
foreach (var file in files
|
foreach (var file in files
|
||||||
.Where(file => !file.EsReport && !file.Subido)
|
.Where(file => !file.EsReport && !file.Subido)
|
||||||
@@ -1341,15 +1330,40 @@ else
|
|||||||
: "Sin historico local";
|
: "Sin historico local";
|
||||||
}
|
}
|
||||||
|
|
||||||
private string GetAssignedGroupLinkBySelectedGroup()
|
private static string NormalizeUpdateGroup(string? groupCode)
|
||||||
|
=> groupCode == "510" ? "510" : "600";
|
||||||
|
|
||||||
|
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",
|
try
|
||||||
"600" => "https://02.g3stiona.com/rest/groups/454fa4ec-8b82-4240-9419-113f45d4b004",
|
{
|
||||||
"700" => "https://02.g3stiona.com/rest/groups/a4ad4dfb-70dc-4219-8ee3-4dcc939f0955",
|
await ApiDenuncias.RegisterGestionaUploadHistoryAsync(
|
||||||
_ => throw new InvalidOperationException($"Grupo desconocido: {selectedGroup}")
|
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)
|
private async Task SincronizarExpedienteGestionaAsync(DenunciasGestiona denuncia, string fileUrl)
|
||||||
@@ -1385,3 +1399,4 @@ else
|
|||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -144,9 +144,9 @@
|
|||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Expediente</th>
|
<th>Expediente</th>
|
||||||
<th>Asunto</th>
|
<th>T<EFBFBD>tulo expediente</th>
|
||||||
<th>Fecha creaci<63>n</th>
|
<th>Fecha creaci<63>n</th>
|
||||||
<th>Estado</th>
|
<th><EFBFBD>ltima actividad Gestiona</th>
|
||||||
<th></th>
|
<th></th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
@@ -157,11 +157,11 @@
|
|||||||
<td>@exp.CodigoExpediente</td>
|
<td>@exp.CodigoExpediente</td>
|
||||||
<td>@exp.Asunto</td>
|
<td>@exp.Asunto</td>
|
||||||
<td>@(exp.FechaCreacion?.ToLocalTime().ToString("dd/MM/yyyy HH:mm"))</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">
|
<td class="text-end">
|
||||||
<button class="btn btn-sm btn-outline-primary"
|
<button class="btn btn-sm btn-outline-primary"
|
||||||
@onclick="() => ToggleDetalle(exp)">
|
@onclick="() => ToggleDetalle(exp)">
|
||||||
@(expedienteSeleccionado == exp ? "Ocultar" : "Abrir expediente")
|
@(expedienteSeleccionado == exp ? "Ocultar" : "Consultar expediente Gestiona")
|
||||||
</button>
|
</button>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -172,26 +172,30 @@
|
|||||||
<td colspan="5">
|
<td colspan="5">
|
||||||
<dl class="row mb-0">
|
<dl class="row mb-0">
|
||||||
<dt class="col-sm-2">Expediente</dt>
|
<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>
|
<dt class="col-sm-2">T<EFBFBD>tulo expediente</dt>
|
||||||
<dd class="col-sm-10">@exp.Asunto</dd>
|
<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>
|
<dt class="col-sm-2">Fecha creaci<63>n</dt>
|
||||||
<dd class="col-sm-10">
|
<dd class="col-sm-10">
|
||||||
@exp.FechaCreacion?.ToLocalTime().ToString("dd/MM/yyyy HH:mm")
|
@(exp.FechaCreacion?.ToLocalTime().ToString("dd/MM/yyyy HH:mm") ?? "-")
|
||||||
</dd>
|
</dd>
|
||||||
|
|
||||||
<dt class="col-sm-2">Estado</dt>
|
<dt class="col-sm-2"><EFBFBD>ltima actividad en Gestiona</dt>
|
||||||
<dd class="col-sm-10">@exp.Estado</dd>
|
|
||||||
|
|
||||||
@if (!string.IsNullOrWhiteSpace(exp.FileUrl))
|
|
||||||
{
|
|
||||||
<dt class="col-sm-2">Enlace Gestiona</dt>
|
|
||||||
<dd class="col-sm-10">
|
<dd class="col-sm-10">
|
||||||
<a href="@exp.FileUrl" target="_blank">@exp.FileUrl</a>
|
@(exp.FechaUltimaModificacion?.ToLocalTime().ToString("dd/MM/yyyy HH:mm") ?? "-")
|
||||||
</dd>
|
</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>
|
</dl>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -233,6 +237,8 @@
|
|||||||
|
|
||||||
private bool IsModo(string valor) => string.Equals(modoFecha, valor, StringComparison.Ordinal);
|
private bool IsModo(string valor) => string.Equals(modoFecha, valor, StringComparison.Ordinal);
|
||||||
private void SetModo(string valor) => modoFecha = valor;
|
private void SetModo(string valor) => modoFecha = valor;
|
||||||
|
private static string FormatDetailValue(string? value)
|
||||||
|
=> string.IsNullOrWhiteSpace(value) ? "-" : value;
|
||||||
|
|
||||||
private void ToggleDetalle(ExpedienteTerceroDto exp)
|
private void ToggleDetalle(ExpedienteTerceroDto exp)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -4,14 +4,34 @@
|
|||||||
|
|
||||||
@using System.Globalization
|
@using System.Globalization
|
||||||
@using GestionaDenunciasAN.Services
|
@using GestionaDenunciasAN.Services
|
||||||
|
@using Microsoft.AspNetCore.Components.Authorization
|
||||||
|
|
||||||
@inject ApiDenunciasClient ApiDenuncias
|
@inject ApiDenunciasClient ApiDenuncias
|
||||||
|
@inject AuthenticationStateProvider AuthenticationStateProvider
|
||||||
@inject UiBusyService Busy
|
@inject UiBusyService Busy
|
||||||
|
|
||||||
<PageTitle>Configuracion</PageTitle>
|
<PageTitle>Configuracion</PageTitle>
|
||||||
|
|
||||||
<h3>Configuracion</h3>
|
<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 mt-3">
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<h5 class="mb-3">Fecha de actualizaciones externas</h5>
|
<h5 class="mb-3">Fecha de actualizaciones externas</h5>
|
||||||
@@ -79,6 +99,21 @@
|
|||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<h5 class="mb-3">Purga manual con reemplazo</h5>
|
<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="alert alert-danger">
|
||||||
<div class="d-flex gap-3">
|
<div class="d-flex gap-3">
|
||||||
<span class="bi bi-exclamation-triangle-fill fs-3" aria-hidden="true"></span>
|
<span class="bi bi-exclamation-triangle-fill fs-3" aria-hidden="true"></span>
|
||||||
@@ -169,11 +204,22 @@
|
|||||||
<div>@purgeErrorMessage</div>
|
<div>@purgeErrorMessage</div>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@code {
|
@code {
|
||||||
private const string RequiredConfirmation = "PURGAR CLAVE ACTUAL";
|
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 externalUpdateCutoffDate = string.Empty;
|
||||||
|
private string latestEncryptionKeyDate = string.Empty;
|
||||||
|
private string latestEncryptionKeyStatus = string.Empty;
|
||||||
private string? configurationNotice;
|
private string? configurationNotice;
|
||||||
private string? configurationError;
|
private string? configurationError;
|
||||||
private bool isSavingConfiguration;
|
private bool isSavingConfiguration;
|
||||||
@@ -191,6 +237,14 @@
|
|||||||
|
|
||||||
protected override async Task OnInitializedAsync()
|
protected override async Task OnInitializedAsync()
|
||||||
{
|
{
|
||||||
|
var authState = await AuthenticationStateProvider.GetAuthenticationStateAsync();
|
||||||
|
hasConfigurationAccess = IsConfigurationUser(authState.User.Identity?.Name);
|
||||||
|
hasCheckedConfigurationAccess = true;
|
||||||
|
if (!hasConfigurationAccess)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
await LoadConfigurationAsync();
|
await LoadConfigurationAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -201,6 +255,8 @@
|
|||||||
configurationError = null;
|
configurationError = null;
|
||||||
var config = await ApiDenuncias.GetAppConfigurationAsync();
|
var config = await ApiDenuncias.GetAppConfigurationAsync();
|
||||||
externalUpdateCutoffDate = ToSpanishDateText(config.ExternalUpdateCutoffDate);
|
externalUpdateCutoffDate = ToSpanishDateText(config.ExternalUpdateCutoffDate);
|
||||||
|
latestEncryptionKeyDate = ToSpanishDateText(config.LatestEncryptionKeyDate);
|
||||||
|
latestEncryptionKeyStatus = ToSpanishKeyStatus(config.LatestEncryptionKeyStatus);
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
@@ -219,6 +275,8 @@
|
|||||||
|
|
||||||
var config = await ApiDenuncias.UpdateExternalUpdateCutoffDateAsync(date);
|
var config = await ApiDenuncias.UpdateExternalUpdateCutoffDateAsync(date);
|
||||||
externalUpdateCutoffDate = ToSpanishDateText(config.ExternalUpdateCutoffDate);
|
externalUpdateCutoffDate = ToSpanishDateText(config.ExternalUpdateCutoffDate);
|
||||||
|
latestEncryptionKeyDate = ToSpanishDateText(config.LatestEncryptionKeyDate);
|
||||||
|
latestEncryptionKeyStatus = ToSpanishKeyStatus(config.LatestEncryptionKeyStatus);
|
||||||
configurationNotice = string.IsNullOrWhiteSpace(externalUpdateCutoffDate)
|
configurationNotice = string.IsNullOrWhiteSpace(externalUpdateCutoffDate)
|
||||||
? "Fecha de corte eliminada."
|
? "Fecha de corte eliminada."
|
||||||
: $"Fecha de corte guardada: {externalUpdateCutoffDate}.";
|
: $"Fecha de corte guardada: {externalUpdateCutoffDate}.";
|
||||||
@@ -287,6 +345,25 @@
|
|||||||
: value.Trim();
|
: 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()
|
private async Task ExecutePurgeAsync()
|
||||||
{
|
{
|
||||||
if (!CanExecutePurge)
|
if (!CanExecutePurge)
|
||||||
@@ -305,6 +382,7 @@
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
purgeResult = await ApiDenuncias.ExecuteCurrentManualPurgeAsync();
|
purgeResult = await ApiDenuncias.ExecuteCurrentManualPurgeAsync();
|
||||||
|
await LoadConfigurationAsync();
|
||||||
acceptedRisk = false;
|
acceptedRisk = false;
|
||||||
confirmation = string.Empty;
|
confirmation = string.Empty;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
@page "/GestionZip"
|
@page "/Entrada"
|
||||||
@rendermode @(new InteractiveServerRenderMode(prerender: false))
|
@rendermode @(new InteractiveServerRenderMode(prerender: false))
|
||||||
@attribute [Authorize]
|
@attribute [Authorize]
|
||||||
@implements IAsyncDisposable
|
@implements IAsyncDisposable
|
||||||
@using System.Globalization
|
@using System.Globalization
|
||||||
|
@using System.Text.RegularExpressions
|
||||||
@using GestionaDenunciasAN.Models
|
@using GestionaDenunciasAN.Models
|
||||||
@inject AuthenticationStateProvider AuthenticationStateProvider
|
@inject AuthenticationStateProvider AuthenticationStateProvider
|
||||||
@inject ApiDenunciasClient ApiDenuncias
|
@inject ApiDenunciasClient ApiDenuncias
|
||||||
@@ -73,9 +74,81 @@
|
|||||||
.report-detail-close {
|
.report-detail-close {
|
||||||
flex: 0 0 auto;
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
.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>
|
</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 class="d-flex flex-column flex-lg-row justify-content-between align-items-lg-center gap-3 mb-4">
|
||||||
<div>
|
<div>
|
||||||
<h3 class="mb-1">Entrada de denuncias</h3>
|
<h3 class="mb-1">Entrada de denuncias</h3>
|
||||||
@@ -96,26 +169,25 @@
|
|||||||
<div class="alert @FilterWarningCss mb-4">@FilterWarningMessage</div>
|
<div class="alert @FilterWarningCss mb-4">@FilterWarningMessage</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
<div class="row g-4">
|
<div class="card shadow-sm mb-3">
|
||||||
<div class="col-lg-4">
|
<div class="card-body py-3">
|
||||||
<div class="card shadow-sm h-100">
|
<div class="row g-3 align-items-end">
|
||||||
<div class="card-body">
|
<div class="col-xl-5 col-lg-6">
|
||||||
<h5 class="card-title">Sesion GlobalLeaks</h5>
|
<h5 class="card-title mb-2">Sesion GlobalLeaks</h5>
|
||||||
<p class="mb-2"><strong>Usuario:</strong> @CurrentUsername</p>
|
<div class="d-flex flex-wrap gap-3 small">
|
||||||
<p class="mb-2"><strong>Estado:</strong> @SessionStatusText</p>
|
<span><strong>Usuario:</strong> @CurrentUsername</span>
|
||||||
<p class="mb-2">
|
<span><strong>Estado:</strong> @SessionStatusText</span>
|
||||||
<strong>Ultima descarga registrada:</strong>
|
<span>
|
||||||
|
<strong>Ultima descarga:</strong>
|
||||||
@(UserInboxState.LastDownloadedReportMomentUtc is null
|
@(UserInboxState.LastDownloadedReportMomentUtc is null
|
||||||
? "Sin descargas previas"
|
? "Sin descargas previas"
|
||||||
: UserInboxState.LastDownloadedReportMomentUtc.Value.ToLocalTime().ToString("dd/MM/yyyy HH:mm"))
|
: UserInboxState.LastDownloadedReportMomentUtc.Value.ToLocalTime().ToString("dd/MM/yyyy HH:mm"))
|
||||||
</p>
|
</span>
|
||||||
<p class="text-muted small mb-4">
|
</div>
|
||||||
La base de datos lleva la cuenta de lo que ya ha descargado este usuario, de lo que han descargado otros
|
</div>
|
||||||
y de si el expediente ya esta creado en Gestiona.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<div class="mb-3">
|
<div class="col-xl-5 col-lg-4">
|
||||||
<label class="form-label">Nuevo codigo 2FA</label>
|
<label class="form-label mb-1">Nuevo codigo 2FA</label>
|
||||||
<input class="form-control"
|
<input class="form-control"
|
||||||
@bind="RenewAuthcode"
|
@bind="RenewAuthcode"
|
||||||
maxlength="6"
|
maxlength="6"
|
||||||
@@ -124,14 +196,15 @@
|
|||||||
placeholder="123456" />
|
placeholder="123456" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="col-xl-2 col-lg-2">
|
||||||
<button type="button" class="btn btn-primary w-100" @onclick="RenewSessionAsync" disabled="@RenewBusy">
|
<button type="button" class="btn btn-primary w-100" @onclick="RenewSessionAsync" disabled="@RenewBusy">
|
||||||
@(RenewBusy ? SessionRenewBusyText : SessionRenewButtonText)
|
@(RenewBusy ? SessionRenewBusyText : SessionRenewButtonText)
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="col-lg-8">
|
|
||||||
<div class="card shadow-sm">
|
<div class="card shadow-sm">
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<div class="d-flex flex-column flex-md-row justify-content-between align-items-md-center gap-3 mb-3">
|
<div class="d-flex flex-column flex-md-row justify-content-between align-items-md-center gap-3 mb-3">
|
||||||
@@ -152,7 +225,7 @@
|
|||||||
<select class="form-select" value="@Filter" @onchange="OnFilterChanged">
|
<select class="form-select" value="@Filter" @onchange="OnFilterChanged">
|
||||||
<option value="all">Todas</option>
|
<option value="all">Todas</option>
|
||||||
<option value="new">Nuevas / sin leer</option>
|
<option value="new">Nuevas / sin leer</option>
|
||||||
<option value="updated">Actualizaciones</option>
|
<option value="updated">Actualizaciones del ciudadano</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -206,6 +279,12 @@
|
|||||||
<div class="d-flex flex-column flex-md-row justify-content-between align-items-md-center gap-3 mb-3">
|
<div class="d-flex flex-column flex-md-row justify-content-between align-items-md-center gap-3 mb-3">
|
||||||
<div class="text-muted">
|
<div class="text-muted">
|
||||||
@VisibleReports.Count denuncia(s) visibles, @SelectedReportsCount seleccionada(s).
|
@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)
|
@if (UserInboxState.LastDownloadedReportMomentUtc is not null)
|
||||||
{
|
{
|
||||||
<span class="d-block small">
|
<span class="d-block small">
|
||||||
@@ -214,7 +293,7 @@
|
|||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
<div class="d-flex gap-2">
|
<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
|
@SelectAllLabel
|
||||||
</button>
|
</button>
|
||||||
<button type="button" class="btn btn-success btn-sm" @onclick="ImportSelectedAsync" disabled="@(!CanImportSelected)">
|
<button type="button" class="btn btn-success btn-sm" @onclick="ImportSelectedAsync" disabled="@(!CanImportSelected)">
|
||||||
@@ -223,25 +302,27 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="table-responsive">
|
<div class="table-responsive inbox-table-wrap">
|
||||||
<table class="table table-hover align-middle">
|
<table class="table table-hover table-sm align-middle inbox-table">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th style="width: 3rem;"></th>
|
<th style="width: 3rem;"></th>
|
||||||
<th>#</th>
|
<th>#</th>
|
||||||
<th>Canal</th>
|
<th>Canal</th>
|
||||||
<th>Presentacion</th>
|
<th>Presentacion</th>
|
||||||
<th>Ultima actualizacion</th>
|
<th>Actividad ciudadano</th>
|
||||||
|
<th>Actividad OAAF</th>
|
||||||
<th>Estado</th>
|
<th>Estado</th>
|
||||||
|
<th>Acceso</th>
|
||||||
<th>Seguimiento</th>
|
<th>Seguimiento</th>
|
||||||
<th style="width: 7rem;">Detalle</th>
|
<th class="inbox-action-cell">Detalle</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
@if (!CanUseGlobalLeaks)
|
@if (!CanUseGlobalLeaks)
|
||||||
{
|
{
|
||||||
<tr>
|
<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.
|
Renueva la sesion de GlobalLeaks con un 2FA valido para cargar la bandeja.
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -249,13 +330,13 @@
|
|||||||
else if (ReportsBusy)
|
else if (ReportsBusy)
|
||||||
{
|
{
|
||||||
<tr>
|
<tr>
|
||||||
<td colspan="8" class="text-muted">Cargando denuncias...</td>
|
<td colspan="10" class="text-muted">Cargando denuncias...</td>
|
||||||
</tr>
|
</tr>
|
||||||
}
|
}
|
||||||
else if (!VisibleReports.Any())
|
else if (!VisibleReports.Any())
|
||||||
{
|
{
|
||||||
<tr>
|
<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>
|
</tr>
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
@@ -266,33 +347,35 @@
|
|||||||
<td>
|
<td>
|
||||||
<input type="checkbox"
|
<input type="checkbox"
|
||||||
checked="@SelectedIds.Contains(report.Id)"
|
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>
|
||||||
<td><strong>#@(report.Progressive ?? 0)</strong></td>
|
<td class="inbox-report-cell"><strong>#@(report.Progressive ?? 0)</strong></td>
|
||||||
<td>@(report.ContextName ?? report.ContextId ?? "-")</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.CreationDate)</td>
|
||||||
<td>@FormatDate(report.UpdateDate)</td>
|
<td class="inbox-activity-cell">@FormatCitizenActivity(report)</td>
|
||||||
|
<td class="inbox-activity-cell">@FormatReceiverActivity(report)</td>
|
||||||
<td>
|
<td>
|
||||||
<span class="badge @GetStatusBadgeCss(report)">
|
<span class="badge @GetStatusBadgeCss(report)">
|
||||||
@GetStatusLabel(report)
|
@GetStatusLabel(report)
|
||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<span class="badge @GetTrackingBadgeCss(report)">
|
<span class="badge @GetAccessBadgeCss(report)">
|
||||||
@GetTrackingLabel(report)
|
@GetAccessLabel(report)
|
||||||
</span>
|
</span>
|
||||||
@if (!string.IsNullOrWhiteSpace(report.TrackingNote))
|
|
||||||
{
|
|
||||||
<div class="small text-muted mt-1">@report.TrackingNote</div>
|
|
||||||
}
|
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td class="inbox-tracking-cell" title="@(report.TrackingNote ?? string.Empty)">
|
||||||
|
<span class="badge @GetTrackingBadgeCss(report)">@GetTrackingLabel(report)</span>
|
||||||
|
</td>
|
||||||
|
<td class="inbox-action-cell">
|
||||||
<button type="button"
|
<button type="button"
|
||||||
class="btn btn-outline-secondary btn-sm"
|
class="btn btn-outline-secondary btn-sm inbox-detail-button"
|
||||||
title="Consulta mensajes y ficheros. GlobalLeaks puede marcar la denuncia como leida."
|
title="@GetReportDetailTitle(report)"
|
||||||
@onclick="@(() => OpenReportDetailAsync(report))"
|
@onclick="@(() => OpenReportDetailAsync(report))"
|
||||||
disabled="@DetailBusy">
|
disabled="@(DetailBusy || report.Accessible == false)">
|
||||||
Ver detalle
|
Detalle
|
||||||
</button>
|
</button>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -303,8 +386,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@if (DetailModalVisible)
|
@if (DetailModalVisible)
|
||||||
@@ -451,7 +532,7 @@
|
|||||||
|
|
||||||
private bool CanUseGlobalLeaks => SessionInfo?.HasActiveSession == true;
|
private bool CanUseGlobalLeaks => SessionInfo?.HasActiveSession == true;
|
||||||
private bool RenewPrepared => !string.IsNullOrWhiteSpace(RenewPendingLoginId);
|
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 bool CanImportSelected => CanUseGlobalLeaks && SelectedReportsCount > 0 && !ImportBusy;
|
||||||
private string SessionStatusText => SessionInfo is null
|
private string SessionStatusText => SessionInfo is null
|
||||||
? "Sin credenciales guardadas"
|
? "Sin credenciales guardadas"
|
||||||
@@ -464,7 +545,7 @@
|
|||||||
private string SessionRenewBusyText => RenewPrepared
|
private string SessionRenewBusyText => RenewPrepared
|
||||||
? "Validando 2FA..."
|
? "Validando 2FA..."
|
||||||
: "Preparando...";
|
: "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"
|
? "Deseleccionar todas"
|
||||||
: "Seleccionar todas";
|
: "Seleccionar todas";
|
||||||
|
|
||||||
@@ -631,9 +712,16 @@
|
|||||||
{
|
{
|
||||||
var selectedReports = Reports
|
var selectedReports = Reports
|
||||||
.Where(report => SelectedIds.Contains(report.Id))
|
.Where(report => SelectedIds.Contains(report.Id))
|
||||||
|
.Where(CanUseReport)
|
||||||
.OrderBy(report => report.Progressive ?? 0)
|
.OrderBy(report => report.Progressive ?? 0)
|
||||||
.ToList();
|
.ToList();
|
||||||
|
|
||||||
|
if (selectedReports.Count == 0)
|
||||||
|
{
|
||||||
|
SetStatus("No hay denuncias accesibles seleccionadas para importar.", "alert-warning");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
using var busy = Busy.Show(
|
using var busy = Busy.Show(
|
||||||
"Importando denuncias",
|
"Importando denuncias",
|
||||||
$"Descargando y procesando {selectedReports.Count} denuncia(s) desde GlobalLeaks.",
|
$"Descargando y procesando {selectedReports.Count} denuncia(s) desde GlobalLeaks.",
|
||||||
@@ -685,6 +773,20 @@
|
|||||||
{
|
{
|
||||||
SetStatus($"Se han importado {importedCount} denuncia(s) desde GlobalLeaks.", "alert-success");
|
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
|
else
|
||||||
{
|
{
|
||||||
var parts = new List<string>
|
var parts = new List<string>
|
||||||
@@ -713,6 +815,12 @@
|
|||||||
|
|
||||||
private async Task OpenReportDetailAsync(ReportDto report)
|
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)
|
if (!CanUseGlobalLeaks)
|
||||||
{
|
{
|
||||||
SetStatus("Renueva antes la sesion de GlobalLeaks para consultar el detalle.", "alert-warning");
|
SetStatus("Renueva antes la sesion de GlobalLeaks para consultar el detalle.", "alert-warning");
|
||||||
@@ -790,7 +898,7 @@
|
|||||||
filtered = Filter switch
|
filtered = Filter switch
|
||||||
{
|
{
|
||||||
"new" => filtered.Where(report => string.IsNullOrWhiteSpace(report.AccessDate) || string.Equals(report.Status, "new", StringComparison.OrdinalIgnoreCase)),
|
"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.Updated),
|
||||||
_ => filtered
|
_ => filtered
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -813,7 +921,10 @@
|
|||||||
.OrderByDescending(report => report.Progressive ?? 0)
|
.OrderByDescending(report => report.Progressive ?? 0)
|
||||||
.ToList();
|
.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));
|
SelectedIds.RemoveWhere(id => !validIds.Contains(id));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -887,16 +998,23 @@
|
|||||||
return reports;
|
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);
|
var isChecked = GetCheckedValue(args);
|
||||||
if (isChecked)
|
if (isChecked)
|
||||||
{
|
{
|
||||||
SelectedIds.Add(reportId);
|
SelectedIds.Add(report.Id);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
SelectedIds.Remove(reportId);
|
SelectedIds.Remove(report.Id);
|
||||||
}
|
}
|
||||||
|
|
||||||
StateHasChanged();
|
StateHasChanged();
|
||||||
@@ -904,9 +1022,12 @@
|
|||||||
|
|
||||||
private void ToggleSelectAll()
|
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)
|
if (shouldSelect)
|
||||||
{
|
{
|
||||||
@@ -980,15 +1101,58 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
private static string FormatDate(string? value)
|
private static string FormatDate(string? value)
|
||||||
|
{
|
||||||
|
return FormatOptionalDate(value) ?? "-";
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string? FormatOptionalDate(string? value)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrWhiteSpace(value))
|
if (string.IsNullOrWhiteSpace(value))
|
||||||
{
|
{
|
||||||
return "-";
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return DateTimeOffset.TryParse(value, out var parsed)
|
return DateTimeOffset.TryParse(value, out var parsed)
|
||||||
? parsed.ToLocalTime().ToString("dd/MM/yyyy HH:mm")
|
? 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 receiverDate;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!report.ActivityAnalyzed)
|
||||||
|
{
|
||||||
|
return "No analizada";
|
||||||
|
}
|
||||||
|
|
||||||
|
return "Sin actividad";
|
||||||
}
|
}
|
||||||
|
|
||||||
private static string FormatBytes(long? value)
|
private static string FormatBytes(long? value)
|
||||||
@@ -1013,15 +1177,42 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
private static string GetCommentAuthorLabel(string? type)
|
private static string GetCommentAuthorLabel(string? type)
|
||||||
=> string.Equals(type, "whistleblower", StringComparison.OrdinalIgnoreCase)
|
=> IsWhistleblowerActivityType(type)
|
||||||
? "Denunciante"
|
? "Denunciante"
|
||||||
: string.Equals(type, "receiver", StringComparison.OrdinalIgnoreCase)
|
: IsReceiverActivityType(type)
|
||||||
? "Receptor"
|
? "Receptor"
|
||||||
: "Comentario";
|
: "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)
|
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)
|
private static DateTimeOffset? ParseDate(string? value)
|
||||||
@@ -1043,6 +1234,16 @@
|
|||||||
return "Sin leer";
|
return "Sin leer";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (report.CitizenHasNewActivity)
|
||||||
|
{
|
||||||
|
return "Actualizacion ciudadano";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (IsReceiverOnlyUpdate(report))
|
||||||
|
{
|
||||||
|
return "Actividad OAAF";
|
||||||
|
}
|
||||||
|
|
||||||
if (report.Updated)
|
if (report.Updated)
|
||||||
{
|
{
|
||||||
return "Actualizada";
|
return "Actualizada";
|
||||||
@@ -1060,16 +1261,81 @@
|
|||||||
return "bg-warning text-dark";
|
return "bg-warning text-dark";
|
||||||
}
|
}
|
||||||
|
|
||||||
if (report.Updated)
|
if (report.CitizenHasNewActivity)
|
||||||
{
|
{
|
||||||
return "bg-info text-dark";
|
return "bg-info text-dark";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (IsReceiverOnlyUpdate(report))
|
||||||
|
{
|
||||||
|
return "bg-secondary";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (report.Updated)
|
||||||
|
{
|
||||||
|
return "bg-light text-dark";
|
||||||
|
}
|
||||||
|
|
||||||
return string.Equals(report.Status, "closed", StringComparison.OrdinalIgnoreCase)
|
return string.Equals(report.Status, "closed", StringComparison.OrdinalIgnoreCase)
|
||||||
? "bg-secondary"
|
? "bg-secondary"
|
||||||
: "bg-primary";
|
: "bg-primary";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 bool CanUseReport(ReportDto report)
|
||||||
|
=> report.Accessible != false && !IsReceiverOnlyUpdate(report);
|
||||||
|
|
||||||
|
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.";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (IsReceiverOnlyUpdate(report))
|
||||||
|
{
|
||||||
|
return "La actividad nueva procede de OAAF/gestor; no se importa como actualizacion del ciudadano.";
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string GetReportDetailTitle(ReportDto report)
|
||||||
|
{
|
||||||
|
if (report.Accessible == false)
|
||||||
|
{
|
||||||
|
return "GlobalLeaks indica que esta denuncia no es accesible para este usuario.";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (IsReceiverOnlyUpdate(report))
|
||||||
|
{
|
||||||
|
return "Actividad interna detectada; puedes consultar el detalle, pero no importarla como actualizacion.";
|
||||||
|
}
|
||||||
|
|
||||||
|
return "Consulta mensajes y ficheros. GlobalLeaks puede marcar la denuncia como leida.";
|
||||||
|
}
|
||||||
|
|
||||||
private static string GetTrackingLabel(ReportDto report)
|
private static string GetTrackingLabel(ReportDto report)
|
||||||
{
|
{
|
||||||
if (report.AlreadyInGestiona)
|
if (report.AlreadyInGestiona)
|
||||||
@@ -1122,6 +1388,16 @@
|
|||||||
|
|
||||||
private static string? GetReportRowCss(ReportDto report)
|
private static string? GetReportRowCss(ReportDto report)
|
||||||
{
|
{
|
||||||
|
if (report.Accessible == false)
|
||||||
|
{
|
||||||
|
return "table-danger report-row-disabled";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (IsReceiverOnlyUpdate(report))
|
||||||
|
{
|
||||||
|
return "table-secondary report-row-disabled";
|
||||||
|
}
|
||||||
|
|
||||||
if (report.AlreadyInGestiona)
|
if (report.AlreadyInGestiona)
|
||||||
{
|
{
|
||||||
return "table-success";
|
return "table-success";
|
||||||
@@ -3,14 +3,16 @@
|
|||||||
@attribute [Authorize]
|
@attribute [Authorize]
|
||||||
@using GestionaDenunciasAN.Models
|
@using GestionaDenunciasAN.Models
|
||||||
@using GestionaDenunciasAN.Services
|
@using GestionaDenunciasAN.Services
|
||||||
|
@using GestionaDenuncias.Shared.Models
|
||||||
@using System.Globalization
|
@using System.Globalization
|
||||||
@attribute [StreamRendering]
|
@attribute [StreamRendering]
|
||||||
@inject GestionaDenunciasAN.Models.UserState userState
|
@inject GestionaDenunciasAN.Models.UserState userState
|
||||||
@inject NavigationManager Navigation
|
@inject NavigationManager Navigation
|
||||||
@inject IHostEnvironment HostEnvironment
|
@inject IHostEnvironment HostEnvironment
|
||||||
@inject IDenunciaStore DenunciaStore
|
@inject IDenunciaStore DenunciaStore
|
||||||
|
@inject ApiDenunciasClient ApiDenuncias
|
||||||
|
|
||||||
<PageTitle>Denuncias Gesti<EFBFBD>n</PageTitle>
|
<PageTitle>Denuncias Gestión</PageTitle>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
/* Contenedor para la lista de denuncias */
|
/* Contenedor para la lista de denuncias */
|
||||||
@@ -64,7 +66,7 @@
|
|||||||
.card-body {
|
.card-body {
|
||||||
padding: 1.25rem;
|
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 {
|
.section-heading {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
@@ -75,9 +77,9 @@
|
|||||||
}
|
}
|
||||||
</style>
|
</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"
|
<input type="text"
|
||||||
class="form-control"
|
class="form-control"
|
||||||
placeholder="Buscar denuncias..."
|
placeholder="Buscar denuncias..."
|
||||||
@@ -89,9 +91,10 @@
|
|||||||
{
|
{
|
||||||
<div class="alert alert-info">Cargando datos...</div>
|
<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
|
else
|
||||||
{
|
{
|
||||||
@@ -105,18 +108,47 @@ else
|
|||||||
))
|
))
|
||||||
{
|
{
|
||||||
var collapseId = $"collapse{denuncia.Id_Denuncia}";
|
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 collapse-card Aceptada">
|
||||||
<div class="card-header" data-bs-toggle="collapse" data-bs-target="#@collapseId" aria-expanded="false" aria-controls="@collapseId">
|
<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>
|
<h5 class="mb-0">Denuncia ID: @denuncia.Id_Denuncia</h5>
|
||||||
<div class="header-info">
|
<div class="header-info">
|
||||||
<span><strong>Estado:</strong> @denuncia.Estado</span>
|
<span><strong>Asunto:</strong> @GetHeaderSubject(denuncia, latestHistory)</span>
|
||||||
<span><strong>Asunto:</strong> @denuncia.NombreDenuncia</span>
|
@if (!string.IsNullOrWhiteSpace(denuncia.ExpedienteGestionaMostrable))
|
||||||
<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><strong>Nº expediente:</strong> @denuncia.ExpedienteGestionaMostrable</span>
|
||||||
|
}
|
||||||
|
@if (!string.IsNullOrWhiteSpace(GetUploadType(denuncia, latestHistory)))
|
||||||
|
{
|
||||||
|
<span><strong>Última subida:</strong> @GetUploadType(denuncia, latestHistory)</span>
|
||||||
|
}
|
||||||
|
@if (!string.IsNullOrWhiteSpace(GetAssignedGroup(denuncia, latestHistory)))
|
||||||
|
{
|
||||||
|
<span><strong>Grupo asignado:</strong> @FormatGroupCode(GetAssignedGroup(denuncia, latestHistory))</span>
|
||||||
|
}
|
||||||
|
@if (!string.IsNullOrWhiteSpace(GetUploadUser(latestHistory)))
|
||||||
|
{
|
||||||
|
<span><strong>Usuario subida:</strong> @GetUploadUser(latestHistory)</span>
|
||||||
|
}
|
||||||
|
<span><strong>Fecha de Subida:</strong> @FormatUploadDate(uploadMoment)</span>
|
||||||
|
<span><strong>Hora de Subida:</strong> @FormatUploadTime(uploadMoment)</span>
|
||||||
|
@if (!string.IsNullOrWhiteSpace(GetAuditDateText(denuncia)))
|
||||||
|
{
|
||||||
|
<span><strong>Última actividad Gestiona:</strong> @GetAuditDateText(denuncia)</span>
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div id="@collapseId" class="collapse">
|
<div id="@collapseId" class="collapse">
|
||||||
<div class="card-body">
|
<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 -->
|
<!-- Datos Generales -->
|
||||||
<h5 class="section-heading">Datos Generales</h5>
|
<h5 class="section-heading">Datos Generales</h5>
|
||||||
<dl class="row">
|
<dl class="row">
|
||||||
@@ -137,12 +169,22 @@ else
|
|||||||
}
|
}
|
||||||
@if (!string.IsNullOrWhiteSpace(denuncia.ExpedienteGestionaMostrable))
|
@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>
|
<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)
|
@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>
|
<dd class="col-sm-9">@denuncia.Id_Persona_Gestiona</dd>
|
||||||
}
|
}
|
||||||
@if (!string.IsNullOrWhiteSpace(denuncia.Etiqueta))
|
@if (!string.IsNullOrWhiteSpace(denuncia.Etiqueta))
|
||||||
@@ -191,13 +233,13 @@ else
|
|||||||
<dd class="col-sm-9">@denuncia.Asunto</dd>
|
<dd class="col-sm-9">@denuncia.Asunto</dd>
|
||||||
<dt class="col-sm-3">A Quien Denuncia</dt>
|
<dt class="col-sm-3">A Quien Denuncia</dt>
|
||||||
<dd class="col-sm-9">@denuncia.A_Quien_Denuncia</dd>
|
<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>
|
<dd class="col-sm-9">@denuncia.Descripcion_Denuncia</dd>
|
||||||
<dt class="col-sm-3">Denunciado Ante Inst</dt>
|
<dt class="col-sm-3">Denunciado Ante Inst</dt>
|
||||||
<dd class="col-sm-9">@denuncia.Denunciado_Ante_Inst</dd>
|
<dd class="col-sm-9">@denuncia.Denunciado_Ante_Inst</dd>
|
||||||
@if (!string.IsNullOrWhiteSpace(denuncia.Modalidad_Informacion))
|
@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>
|
<dd class="col-sm-9">@denuncia.Modalidad_Informacion</dd>
|
||||||
}
|
}
|
||||||
<dt class="col-sm-3">Lugar Hechos</dt>
|
<dt class="col-sm-3">Lugar Hechos</dt>
|
||||||
@@ -209,27 +251,27 @@ else
|
|||||||
}
|
}
|
||||||
</dl>
|
</dl>
|
||||||
|
|
||||||
<!-- Datos de Notificaci<EFBFBD>n -->
|
<!-- Datos de Notificación -->
|
||||||
<h5 class="section-heading">Datos de Notificaci<EFBFBD>n</h5>
|
<h5 class="section-heading">Datos de Notificación</h5>
|
||||||
<dl class="row">
|
<dl class="row">
|
||||||
@if (!string.IsNullOrWhiteSpace(denuncia.Notificacion_Preferencia))
|
@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>
|
<dd class="col-sm-9">@denuncia.Notificacion_Preferencia</dd>
|
||||||
}
|
}
|
||||||
@if (!string.IsNullOrWhiteSpace(denuncia.Notificacion_Electronica))
|
@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>
|
<dd class="col-sm-9">@denuncia.Notificacion_Electronica</dd>
|
||||||
}
|
}
|
||||||
@if (!string.IsNullOrWhiteSpace(denuncia.Correo_Electronico))
|
@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>
|
<dd class="col-sm-9">@denuncia.Correo_Electronico</dd>
|
||||||
}
|
}
|
||||||
@if (!string.IsNullOrWhiteSpace(denuncia.Notificacion_Sms))
|
@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>
|
<dd class="col-sm-9">@denuncia.Notificacion_Sms</dd>
|
||||||
}
|
}
|
||||||
</dl>
|
</dl>
|
||||||
@@ -240,7 +282,7 @@ else
|
|||||||
@if (denuncia.Condiciones)
|
@if (denuncia.Condiciones)
|
||||||
{
|
{
|
||||||
<dt class="col-sm-3">Condiciones</dt>
|
<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))
|
@if (!string.IsNullOrWhiteSpace(denuncia.Comments))
|
||||||
{
|
{
|
||||||
@@ -257,7 +299,7 @@ else
|
|||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Nombre</th>
|
<th>Nombre</th>
|
||||||
<th>Tama<EFBFBD>o (bytes)</th>
|
<th>Tamaño (bytes)</th>
|
||||||
<th>Ver</th>
|
<th>Ver</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
@@ -277,6 +319,29 @@ else
|
|||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</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><strong>Asunto:</strong> @DisplayOrDash(item.Asunto)</span>
|
||||||
|
<span><strong>Nº expediente:</strong> @DisplayOrDash(item.CodigoExpedienteGestiona)</span>
|
||||||
|
<span><strong>Última subida:</strong> @DisplayOrDash(item.TipoSubida)</span>
|
||||||
|
<span><strong>Grupo asignado:</strong> @DisplayOrDash(FormatGroupCode(item.GrupoAsignado))</span>
|
||||||
|
@if (!string.IsNullOrWhiteSpace(GetUploadUser(item)))
|
||||||
|
{
|
||||||
|
<span><strong>Usuario subida:</strong> @GetUploadUser(item)</span>
|
||||||
|
}
|
||||||
|
<span><strong>Fecha de Subida:</strong> @FormatUploadDate(uploadMoment)</span>
|
||||||
|
<span><strong>Hora de Subida:</strong> @FormatUploadTime(uploadMoment)</span>
|
||||||
|
<span class="text-muted">Detalle no disponible por purga criptográfica.</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -305,9 +370,12 @@ else
|
|||||||
|
|
||||||
@code {
|
@code {
|
||||||
private List<DenunciasGestiona> denunciasGestiona = new();
|
private List<DenunciasGestiona> denunciasGestiona = new();
|
||||||
|
private List<GestionaUploadHistoryEntry> historialGestiona = new();
|
||||||
private Dictionary<int, List<FicherosDenuncias>> ficherosAdjuntos = 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 string busqueda = "";
|
||||||
|
|
||||||
private bool hasLoaded = false;
|
private bool hasLoaded = false;
|
||||||
@@ -316,8 +384,10 @@ else
|
|||||||
{
|
{
|
||||||
if (firstRender)
|
if (firstRender)
|
||||||
{
|
{
|
||||||
|
await CargarHistorialGestionaAsync();
|
||||||
await CargarGestionaAsync();
|
await CargarGestionaAsync();
|
||||||
await CargarFicherosAdjuntosAsync(denunciasGestiona.Select(d => d.Id_Denuncia));
|
await CargarFicherosAdjuntosAsync(denunciasGestiona.Select(d => d.Id_Denuncia));
|
||||||
|
await CargarAuditoriasGestionaAsync(denunciasGestiona);
|
||||||
hasLoaded = true;
|
hasLoaded = true;
|
||||||
StateHasChanged();
|
StateHasChanged();
|
||||||
}
|
}
|
||||||
@@ -331,16 +401,206 @@ else
|
|||||||
.ToList();
|
.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()
|
private async Task<List<DenunciasGestiona>> CargarDenunciasJsonAsync()
|
||||||
{
|
{
|
||||||
return await DenunciaStore.GetDenunciasByScopeAsync(DenunciaListScope.InGestiona);
|
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)
|
private async Task CargarFicherosAdjuntosAsync(IEnumerable<int> denunciaIds)
|
||||||
{
|
{
|
||||||
ficherosAdjuntos.Clear();
|
ficherosAdjuntos.Clear();
|
||||||
|
ficherosAdjuntosPurgados.Clear();
|
||||||
foreach (var denunciaId in denunciaIds.Where(id => id > 0).Distinct())
|
foreach (var denunciaId in denunciaIds.Where(id => id > 0).Distinct())
|
||||||
|
{
|
||||||
|
try
|
||||||
{
|
{
|
||||||
var ficheros = await DenunciaStore.GetFicherosByDenunciaAsync(denunciaId);
|
var ficheros = await DenunciaStore.GetFicherosByDenunciaAsync(denunciaId);
|
||||||
if (ficheros.Count > 0)
|
if (ficheros.Count > 0)
|
||||||
@@ -348,6 +608,19 @@ else
|
|||||||
ficherosAdjuntos[denunciaId] = ficheros;
|
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)
|
private static string BuildAttachmentContentUrl(int denunciaId, string? fileName)
|
||||||
|
|||||||
@@ -355,7 +355,7 @@
|
|||||||
return ReturnUrl;
|
return ReturnUrl;
|
||||||
}
|
}
|
||||||
|
|
||||||
return "/GestionZip";
|
return "/Entrada";
|
||||||
}
|
}
|
||||||
|
|
||||||
private static T? ReadData<T>(ApiJsResponse response)
|
private static T? ReadData<T>(ApiJsResponse response)
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
@page "/Pendientes"
|
@page "/Pendientes"
|
||||||
@rendermode InteractiveServer
|
@rendermode InteractiveServer
|
||||||
@attribute [Authorize]
|
@attribute [Authorize]
|
||||||
|
|
||||||
@@ -205,16 +205,7 @@ else
|
|||||||
class="btn btn-success btn-sm me-2"
|
class="btn btn-success btn-sm me-2"
|
||||||
@onclick:stopPropagation="true"
|
@onclick:stopPropagation="true"
|
||||||
@onclick="() => OpenEnviarAGestionaModal(denuncia)">
|
@onclick="() => OpenEnviarAGestionaModal(denuncia)">
|
||||||
Configurar subida
|
Configurar apertura expediente
|
||||||
</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
|
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button type="button"
|
<button type="button"
|
||||||
@@ -247,7 +238,7 @@ else
|
|||||||
}
|
}
|
||||||
@if (!string.IsNullOrWhiteSpace(denuncia.ExpedienteGestionaMostrable))
|
@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>
|
<dd class="col-sm-9">@denuncia.ExpedienteGestionaMostrable</dd>
|
||||||
}
|
}
|
||||||
@if (denuncia.Id_Persona_Gestiona != 0)
|
@if (denuncia.Id_Persona_Gestiona != 0)
|
||||||
@@ -287,7 +278,7 @@ else
|
|||||||
}
|
}
|
||||||
@if (!string.IsNullOrWhiteSpace(denuncia.RazonSocialResuelta))
|
@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>
|
<dd class="col-sm-9">@denuncia.RazonSocialResuelta</dd>
|
||||||
}
|
}
|
||||||
@if (!string.IsNullOrWhiteSpace(denuncia.NombreResuelto))
|
@if (!string.IsNullOrWhiteSpace(denuncia.NombreResuelto))
|
||||||
@@ -297,12 +288,12 @@ else
|
|||||||
}
|
}
|
||||||
@if (!string.IsNullOrWhiteSpace(denuncia.PrimerApellidoResuelto))
|
@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>
|
<dd class="col-sm-9">@denuncia.PrimerApellidoResuelto</dd>
|
||||||
}
|
}
|
||||||
@if (!string.IsNullOrWhiteSpace(denuncia.SegundoApellidoResuelto))
|
@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>
|
<dd class="col-sm-9">@denuncia.SegundoApellidoResuelto</dd>
|
||||||
}
|
}
|
||||||
@if (!string.IsNullOrWhiteSpace(denuncia.ApellidosResueltos))
|
@if (!string.IsNullOrWhiteSpace(denuncia.ApellidosResueltos))
|
||||||
@@ -322,7 +313,7 @@ else
|
|||||||
}
|
}
|
||||||
@if (!string.IsNullOrWhiteSpace(denuncia.PaisOrigen))
|
@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>
|
<dd class="col-sm-9">@denuncia.PaisOrigen</dd>
|
||||||
}
|
}
|
||||||
</dl>
|
</dl>
|
||||||
@@ -339,7 +330,7 @@ else
|
|||||||
<dt class="col-sm-3">Detalle denunciado</dt>
|
<dt class="col-sm-3">Detalle denunciado</dt>
|
||||||
<dd class="col-sm-9">@denuncia.DenunciadoDetalle</dd>
|
<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>
|
<dd class="col-sm-9">@denuncia.Descripcion_Denuncia</dd>
|
||||||
<dt class="col-sm-3">Denunciado Ante Inst</dt>
|
<dt class="col-sm-3">Denunciado Ante Inst</dt>
|
||||||
<dd class="col-sm-9">@denuncia.Denunciado_Ante_Inst</dd>
|
<dd class="col-sm-9">@denuncia.Denunciado_Ante_Inst</dd>
|
||||||
@@ -350,7 +341,7 @@ else
|
|||||||
}
|
}
|
||||||
@if (!string.IsNullOrWhiteSpace(denuncia.SolicitaProteccion))
|
@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>
|
<dd class="col-sm-9">@denuncia.SolicitaProteccion</dd>
|
||||||
}
|
}
|
||||||
@if (!string.IsNullOrWhiteSpace(denuncia.MedidasProteccionSolicitadas))
|
@if (!string.IsNullOrWhiteSpace(denuncia.MedidasProteccionSolicitadas))
|
||||||
@@ -360,7 +351,7 @@ else
|
|||||||
}
|
}
|
||||||
@if (!string.IsNullOrWhiteSpace(denuncia.Modalidad_Informacion))
|
@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>
|
<dd class="col-sm-9">@denuncia.Modalidad_Informacion</dd>
|
||||||
}
|
}
|
||||||
<dt class="col-sm-3">Lugar Hechos</dt>
|
<dt class="col-sm-3">Lugar Hechos</dt>
|
||||||
@@ -372,7 +363,7 @@ else
|
|||||||
}
|
}
|
||||||
@if (!string.IsNullOrWhiteSpace(denuncia.AutorizaRemision))
|
@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>
|
<dd class="col-sm-9">@denuncia.AutorizaRemision</dd>
|
||||||
}
|
}
|
||||||
@if (!string.IsNullOrWhiteSpace(denuncia.PreferenciaRemision))
|
@if (!string.IsNullOrWhiteSpace(denuncia.PreferenciaRemision))
|
||||||
@@ -382,16 +373,16 @@ else
|
|||||||
}
|
}
|
||||||
</dl>
|
</dl>
|
||||||
|
|
||||||
<h5 class="section-heading">Datos de Notificaci<EFBFBD>n</h5>
|
<h5 class="section-heading">Datos de Notificación</h5>
|
||||||
<dl class="row">
|
<dl class="row">
|
||||||
@if (!string.IsNullOrWhiteSpace(denuncia.Notificacion_Preferencia))
|
@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>
|
<dd class="col-sm-9">@denuncia.Notificacion_Preferencia</dd>
|
||||||
}
|
}
|
||||||
@if (!string.IsNullOrWhiteSpace(denuncia.Notificacion_Electronica))
|
@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>
|
<dd class="col-sm-9">@denuncia.Notificacion_Electronica</dd>
|
||||||
}
|
}
|
||||||
@if (!string.IsNullOrWhiteSpace(denuncia.SeguimientoOnline))
|
@if (!string.IsNullOrWhiteSpace(denuncia.SeguimientoOnline))
|
||||||
@@ -406,17 +397,17 @@ else
|
|||||||
}
|
}
|
||||||
@if (!string.IsNullOrWhiteSpace(denuncia.Correo_Electronico))
|
@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>
|
<dd class="col-sm-9">@denuncia.Correo_Electronico</dd>
|
||||||
}
|
}
|
||||||
@if (!string.IsNullOrWhiteSpace(denuncia.Notificacion_Sms))
|
@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>
|
<dd class="col-sm-9">@denuncia.Notificacion_Sms</dd>
|
||||||
}
|
}
|
||||||
@if (HasPostalAddress(denuncia))
|
@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>
|
<dd class="col-sm-9">@BuildPostalAddressSummary(denuncia)</dd>
|
||||||
}
|
}
|
||||||
</dl>
|
</dl>
|
||||||
@@ -426,7 +417,7 @@ else
|
|||||||
@if (denuncia.Condiciones)
|
@if (denuncia.Condiciones)
|
||||||
{
|
{
|
||||||
<dt class="col-sm-3">Condiciones</dt>
|
<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))
|
@if (!string.IsNullOrWhiteSpace(denuncia.Comments))
|
||||||
{
|
{
|
||||||
@@ -441,14 +432,14 @@ else
|
|||||||
@if (camposFormulario.Count > 0)
|
@if (camposFormulario.Count > 0)
|
||||||
{
|
{
|
||||||
<h5 class="section-heading">Formulario Original</h5>
|
<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>
|
<h6 class="mt-3">@grupoCampos.Key</h6>
|
||||||
<dl class="row">
|
<dl class="row">
|
||||||
@foreach (var campo in grupoCampos)
|
@foreach (var campo in grupoCampos)
|
||||||
{
|
{
|
||||||
<dt class="col-sm-4">@campo.Label</dt>
|
<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>
|
</dl>
|
||||||
}
|
}
|
||||||
@@ -463,7 +454,7 @@ else
|
|||||||
<th class="seleccionar-col">Subir</th>
|
<th class="seleccionar-col">Subir</th>
|
||||||
<th>Nombre</th>
|
<th>Nombre</th>
|
||||||
<th>Fecha</th>
|
<th>Fecha</th>
|
||||||
<th>Tama<EFBFBD>o (bytes)</th>
|
<th>Tamaño (bytes)</th>
|
||||||
<th>Ver</th>
|
<th>Ver</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
@@ -541,7 +532,7 @@ else
|
|||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
<h6 class="modal-section-heading">Descripci<EFBFBD>n</h6>
|
<h6 class="modal-section-heading">Descripción</h6>
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<input type="text"
|
<input type="text"
|
||||||
class="form-control"
|
class="form-control"
|
||||||
@@ -554,9 +545,9 @@ else
|
|||||||
<input type="text"
|
<input type="text"
|
||||||
class="form-control"
|
class="form-control"
|
||||||
@bind="nombreDocumentos"
|
@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">
|
<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>
|
</small>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -569,7 +560,7 @@ else
|
|||||||
checked='@(uploadMode == "merge")'
|
checked='@(uploadMode == "merge")'
|
||||||
@onclick='() => uploadMode = "merge"' />
|
@onclick='() => uploadMode = "merge"' />
|
||||||
<label class="form-check-label" for="modoMerge">
|
<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>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-check">
|
<div class="form-check">
|
||||||
@@ -593,7 +584,7 @@ else
|
|||||||
checked='@(selectedGroup == "600")'
|
checked='@(selectedGroup == "600")'
|
||||||
@onclick='() => selectedGroup = "600"' />
|
@onclick='() => selectedGroup = "600"' />
|
||||||
<label class="form-check-label" for="grupo600">
|
<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>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
@* <div class="form-check">
|
@* <div class="form-check">
|
||||||
@@ -604,7 +595,7 @@ else
|
|||||||
checked='@(selectedGroup == "510")'
|
checked='@(selectedGroup == "510")'
|
||||||
@onclick='() => selectedGroup = "510"' />
|
@onclick='() => selectedGroup = "510"' />
|
||||||
<label class="form-check-label" for="grupo510">
|
<label class="form-check-label" for="grupo510">
|
||||||
510. SDI <EFBFBD> Investigaci<EFBFBD>n Entradas
|
510. SDI – Investigación Entradas
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-check">
|
<div class="form-check">
|
||||||
@@ -626,13 +617,13 @@ else
|
|||||||
<h6 class="modal-section-heading mt-3">Tercero (denunciante)</h6>
|
<h6 class="modal-section-heading mt-3">Tercero (denunciante)</h6>
|
||||||
|
|
||||||
<div class="alert alert-light border mb-3">
|
<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>
|
</div>
|
||||||
|
|
||||||
@if (modalThirdParty.IsAnonymous)
|
@if (modalThirdParty.IsAnonymous)
|
||||||
{
|
{
|
||||||
<div class="alert alert-warning mb-3">
|
<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>
|
</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -661,7 +652,7 @@ else
|
|||||||
{
|
{
|
||||||
<div class="row g-2">
|
<div class="row g-2">
|
||||||
<div class="col-12 mb-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 />
|
<input class="form-control" value="@GetReadOnlyValue(selectedDenuncias.RazonSocialResuelta)" readonly />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -674,11 +665,11 @@ else
|
|||||||
<input class="form-control" value="@GetReadOnlyValue(selectedDenuncias.NombreResuelto)" readonly />
|
<input class="form-control" value="@GetReadOnlyValue(selectedDenuncias.NombreResuelto)" readonly />
|
||||||
</div>
|
</div>
|
||||||
<div class="col-4 mb-2">
|
<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 />
|
<input class="form-control" value="@GetReadOnlyValue(selectedDenuncias.PrimerApellidoResuelto)" readonly />
|
||||||
</div>
|
</div>
|
||||||
<div class="col-4 mb-2">
|
<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 />
|
<input class="form-control" value="@GetReadOnlyValue(selectedDenuncias.SegundoApellidoResuelto)" readonly />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -688,14 +679,14 @@ else
|
|||||||
{
|
{
|
||||||
<div class="row g-2">
|
<div class="row g-2">
|
||||||
<div class="col-12 mb-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>
|
<textarea class="form-control" rows="2" readonly>@BuildPostalAddressSummary(selectedDenuncias)</textarea>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
<small class="text-muted">
|
<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>
|
</small>
|
||||||
|
|
||||||
@if (!modalThirdParty.IsAnonymous && !string.IsNullOrWhiteSpace(modalThirdParty.DocumentId))
|
@if (!modalThirdParty.IsAnonymous && !string.IsNullOrWhiteSpace(modalThirdParty.DocumentId))
|
||||||
@@ -768,7 +759,7 @@ else
|
|||||||
{
|
{
|
||||||
<div class="d-flex align-items-center">
|
<div class="d-flex align-items-center">
|
||||||
<div class="spinner-border spinner-border-sm me-2" role="status"></div>
|
<div class="spinner-border spinner-border-sm me-2" role="status"></div>
|
||||||
<span>Cargando expedientes<EFBFBD></span>
|
<span>Cargando expedientes…</span>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
else if (!string.IsNullOrWhiteSpace(errorExpedientes))
|
else if (!string.IsNullOrWhiteSpace(errorExpedientes))
|
||||||
@@ -788,7 +779,7 @@ else
|
|||||||
<tr>
|
<tr>
|
||||||
<th>Expediente</th>
|
<th>Expediente</th>
|
||||||
<th>Asunto</th>
|
<th>Asunto</th>
|
||||||
<th>Fecha creaci<EFBFBD>n</th>
|
<th>Fecha creación</th>
|
||||||
<th>Estado</th>
|
<th>Estado</th>
|
||||||
<th></th>
|
<th></th>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -827,7 +818,7 @@ else
|
|||||||
private string nombreDocumentos = string.Empty;
|
private string nombreDocumentos = string.Empty;
|
||||||
private bool isUploading = false;
|
private bool isUploading = false;
|
||||||
|
|
||||||
private string uploadMode = "merge";
|
private string uploadMode = "individual";
|
||||||
private string selectedGroup = "600";
|
private string selectedGroup = "600";
|
||||||
|
|
||||||
|
|
||||||
@@ -870,14 +861,6 @@ else
|
|||||||
{
|
{
|
||||||
loadError = string.Empty;
|
loadError = string.Empty;
|
||||||
var todas = await CargarDenunciasJsonAsync();
|
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
|
// SOLO pendientes
|
||||||
pendientes = todas
|
pendientes = todas
|
||||||
.Where(d => !d.EnGestiona && !d.EnRechazada && !d.EsActualizacion)
|
.Where(d => !d.EnGestiona && !d.EnRechazada && !d.EsActualizacion)
|
||||||
@@ -944,6 +927,15 @@ else
|
|||||||
return new string(clean.Where(ch => ch <= 127).ToArray());
|
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()
|
private async Task ConfirmarEnvio()
|
||||||
{
|
{
|
||||||
if (selectedDenuncias == null) return;
|
if (selectedDenuncias == null) return;
|
||||||
@@ -988,10 +980,11 @@ else
|
|||||||
{
|
{
|
||||||
operationError = ficherosVacios.Count == 0
|
operationError = ficherosVacios.Count == 0
|
||||||
? $"La denuncia #{selectedDenuncias.Id_Denuncia} no tiene ficheros para subir."
|
? $"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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var expedienteCreadoEnGestiona = false;
|
||||||
string fileUrl;
|
string fileUrl;
|
||||||
if (selectedDenuncias.EnGestiona && !string.IsNullOrWhiteSpace(selectedDenuncias.Expediente_Gestiona))
|
if (selectedDenuncias.EnGestiona && !string.IsNullOrWhiteSpace(selectedDenuncias.Expediente_Gestiona))
|
||||||
{
|
{
|
||||||
@@ -1001,7 +994,6 @@ else
|
|||||||
{
|
{
|
||||||
Busy.Update(message: "Creando expediente nuevo en Gestiona.", detail: "Paso 2 de 7");
|
Busy.Update(message: "Creando expediente nuevo en Gestiona.", detail: "Paso 2 de 7");
|
||||||
var createdFile = await ApiDenuncias.CreateGestionaFileAsync(
|
var createdFile = await ApiDenuncias.CreateGestionaFileAsync(
|
||||||
selectedDenuncias.ProcedureId,
|
|
||||||
nuevoAsunto,
|
nuevoAsunto,
|
||||||
"RQ2ZLC - Expediente de Denuncias",
|
"RQ2ZLC - Expediente de Denuncias",
|
||||||
"3109963"
|
"3109963"
|
||||||
@@ -1011,20 +1003,13 @@ else
|
|||||||
await ApiDenuncias.OpenGestionaFileAsync(
|
await ApiDenuncias.OpenGestionaFileAsync(
|
||||||
fileUrl,
|
fileUrl,
|
||||||
createdFile.FileOpenUrl,
|
createdFile.FileOpenUrl,
|
||||||
managementUnitGroupId: Guid.Parse("a4ad4dfb-70dc-4219-8ee3-4dcc939f0955"),
|
assignedGroupCode: selectedGroup,
|
||||||
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}")
|
|
||||||
},
|
|
||||||
confidential: selectedDenuncias.Confidencial,
|
confidential: selectedDenuncias.Confidencial,
|
||||||
freeTitle: nuevoAsunto,
|
freeTitle: nuevoAsunto
|
||||||
siaCode: "3109963"
|
|
||||||
);
|
);
|
||||||
selectedDenuncias.Expediente_Gestiona = fileUrl;
|
selectedDenuncias.Expediente_Gestiona = fileUrl;
|
||||||
selectedDenuncias.EnGestiona = true;
|
selectedDenuncias.EnGestiona = true;
|
||||||
|
expedienteCreadoEnGestiona = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
Busy.Update(message: "Guardando referencia local del expediente.", detail: "Paso 3 de 7");
|
Busy.Update(message: "Guardando referencia local del expediente.", detail: "Paso 3 de 7");
|
||||||
@@ -1033,7 +1018,7 @@ else
|
|||||||
|
|
||||||
var thirdParty = selectedThirdParty ?? ThirdPartyIdentityData.FromComplaint(selectedDenuncias);
|
var thirdParty = selectedThirdParty ?? ThirdPartyIdentityData.FromComplaint(selectedDenuncias);
|
||||||
Busy.Update(message: "Resolviendo tercero y enlazandolo al expediente.", detail: "Paso 4 de 7");
|
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 nombresOriginalesSubidos = new List<string>();
|
||||||
var nombresFinalesSubidos = new List<string>();
|
var nombresFinalesSubidos = new List<string>();
|
||||||
@@ -1045,8 +1030,8 @@ else
|
|||||||
|
|
||||||
if (!string.IsNullOrWhiteSpace(report.FileName))
|
if (!string.IsNullOrWhiteSpace(report.FileName))
|
||||||
{
|
{
|
||||||
Busy.Update(message: "Preparando y subiendo el report para firma.", detail: "Paso 5 de 7");
|
Busy.Update(message: "Preparando y subiendo el documento principal.", detail: "Paso 5 de 7");
|
||||||
var reportPdfBytes = PdfHelper.MergeFilesToPdf(new[]
|
var reportPdfBytes = PdfHelper.MergeReportToPdf(new[]
|
||||||
{
|
{
|
||||||
(FileName: report.FileName, Content: report.Content)
|
(FileName: report.FileName, Content: report.Content)
|
||||||
});
|
});
|
||||||
@@ -1067,7 +1052,7 @@ else
|
|||||||
{
|
{
|
||||||
Busy.Update(message: "Uniendo adjuntos en un unico PDF y subiendolo.", detail: "Paso 6 de 7");
|
Busy.Update(message: "Uniendo adjuntos en un unico PDF y subiendolo.", detail: "Paso 6 de 7");
|
||||||
var pdfBytes = PdfHelper.MergeFilesToPdf(adjuntos);
|
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);
|
var docUrl = await ApiDenuncias.UploadGestionaDocumentAsync(fileUrl, pdfBytes, pdfName);
|
||||||
if (string.IsNullOrWhiteSpace(documentoParaTramitar))
|
if (string.IsNullOrWhiteSpace(documentoParaTramitar))
|
||||||
{
|
{
|
||||||
@@ -1127,11 +1112,12 @@ else
|
|||||||
|
|
||||||
if (!string.IsNullOrWhiteSpace(documentoParaTramitar))
|
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(
|
await ApiDenuncias.TramitarGestionaDocumentAsync(
|
||||||
documentoParaTramitar,
|
documentoParaTramitar,
|
||||||
GetAssignedGroupLinkBySelectedGroup(),
|
selectedGroup,
|
||||||
selectedDenuncias.Id_Denuncia);
|
selectedDenuncias.Id_Denuncia,
|
||||||
|
isUpdate: false);
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach (var origName in nombresOriginalesSubidos)
|
foreach (var origName in nombresOriginalesSubidos)
|
||||||
@@ -1155,7 +1141,16 @@ else
|
|||||||
selectedDenuncias.EsActualizacion = false;
|
selectedDenuncias.EsActualizacion = false;
|
||||||
selectedDenuncias.NombreDenuncia = nuevoAsunto;
|
selectedDenuncias.NombreDenuncia = nuevoAsunto;
|
||||||
selectedDenuncias.FechaSubidaAGestiona = ahoraUtc;
|
selectedDenuncias.FechaSubidaAGestiona = ahoraUtc;
|
||||||
|
selectedDenuncias.UltimaSubidaGestionaTipo = "Nueva denuncia";
|
||||||
|
selectedDenuncias.UltimoGrupoAsignadoGestiona = GetGestionaGroupDisplay(selectedGroup);
|
||||||
|
await SincronizarExpedienteGestionaAsync(selectedDenuncias, fileUrl);
|
||||||
await ActualizarDenunciaAsync(selectedDenuncias);
|
await ActualizarDenunciaAsync(selectedDenuncias);
|
||||||
|
var historialAviso = await RegistrarHistorialGestionaAsync(
|
||||||
|
selectedDenuncias,
|
||||||
|
"Nueva denuncia",
|
||||||
|
selectedGroup,
|
||||||
|
ahoraUtc,
|
||||||
|
string.Join("; ", nombresFinalesSubidos));
|
||||||
|
|
||||||
var denunciaProcesadaId = selectedDenuncias.Id_Denuncia;
|
var denunciaProcesadaId = selectedDenuncias.Id_Denuncia;
|
||||||
pendientes.Remove(selectedDenuncias);
|
pendientes.Remove(selectedDenuncias);
|
||||||
@@ -1165,22 +1160,35 @@ else
|
|||||||
var avisos = new List<string>();
|
var avisos = new List<string>();
|
||||||
if (ficherosVacios.Count > 0)
|
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)
|
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();
|
StateHasChanged();
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
Console.Error.WriteLine($"Error al confirmar env<EFBFBD>o: {ex}");
|
Console.Error.WriteLine($"Error al confirmar envío: {ex}");
|
||||||
operationError = $"No se ha podido completar el env<EFBFBD>o de la denuncia #{selectedDenuncias?.Id_Denuncia}: {ex.Message}";
|
operationError = $"No se ha podido completar el envío de la denuncia #{selectedDenuncias?.Id_Denuncia}: {ex.Message}";
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
@@ -1226,15 +1234,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",
|
try
|
||||||
"600" => "https://02.g3stiona.com/rest/groups/454fa4ec-8b82-4240-9419-113f45d4b004",
|
{
|
||||||
"700" => "https://02.g3stiona.com/rest/groups/a4ad4dfb-70dc-4219-8ee3-4dcc939f0955",
|
await ApiDenuncias.RegisterGestionaUploadHistoryAsync(
|
||||||
_ => throw new InvalidOperationException($"Grupo desconocido: {selectedGroup}")
|
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()
|
private async Task ConfirmarRechazo()
|
||||||
@@ -1379,7 +1409,7 @@ else
|
|||||||
}
|
}
|
||||||
|
|
||||||
private static string GetReadOnlyValue(string? value) =>
|
private static string GetReadOnlyValue(string? value) =>
|
||||||
string.IsNullOrWhiteSpace(value) ? "<EFBFBD>" : value;
|
string.IsNullOrWhiteSpace(value) ? "—" : value;
|
||||||
|
|
||||||
private static bool HasPostalAddress(DenunciasGestiona denuncia)
|
private static bool HasPostalAddress(DenunciasGestiona denuncia)
|
||||||
{
|
{
|
||||||
@@ -1438,18 +1468,7 @@ else
|
|||||||
return string.Join(" | ", parts);
|
return string.Join(" | ", parts);
|
||||||
}
|
}
|
||||||
|
|
||||||
// mover a Actualizaciones
|
// ========= LÓGICA BUSCADOR DE EXPEDIENTES POR TERCERO =========
|
||||||
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 =========
|
|
||||||
|
|
||||||
private void CloseExpedientesModal()
|
private void CloseExpedientesModal()
|
||||||
{
|
{
|
||||||
@@ -1468,7 +1487,7 @@ else
|
|||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(nif) || nif == "00000000T")
|
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;
|
showExpedientesModal = true;
|
||||||
StateHasChanged();
|
StateHasChanged();
|
||||||
return;
|
return;
|
||||||
@@ -1499,3 +1518,4 @@ else
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -34,6 +34,9 @@ public sealed class ApiDenunciaStore : IDenunciaStore
|
|||||||
public async Task<List<FicherosDenuncias>> GetFicherosByDenunciaAsync(int denunciaId, CancellationToken cancellationToken = default)
|
public async Task<List<FicherosDenuncias>> GetFicherosByDenunciaAsync(int denunciaId, CancellationToken cancellationToken = default)
|
||||||
=> (await _api.GetAsync<List<FicherosDenuncias>>($"api/denuncias/{denunciaId}/ficheros", cancellationToken)) ?? [];
|
=> (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)
|
public Task<DenunciasGestiona?> GetDenunciaByIdAsync(int denunciaId, CancellationToken cancellationToken = default)
|
||||||
=> _api.GetAsync<DenunciasGestiona?>($"api/denuncias/{denunciaId}", cancellationToken);
|
=> _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)
|
public Task UpsertFicherosAsync(IEnumerable<FicherosDenuncias> ficheros, CancellationToken cancellationToken = default)
|
||||||
=> _api.PostAsync("api/denuncias/ficheros", new UpsertFicherosRequest(ficheros.ToArray()), cancellationToken);
|
=> _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(
|
public Task MarkFicherosAsUploadedAsync(
|
||||||
int denunciaId,
|
int denunciaId,
|
||||||
IEnumerable<string> fileNames,
|
IEnumerable<string> fileNames,
|
||||||
|
|||||||
@@ -99,46 +99,35 @@ public sealed class ApiDenunciasClient
|
|||||||
cancellationToken);
|
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(
|
public Task<GestionaCreateFileResponse> CreateGestionaFileAsync(
|
||||||
Guid procedureId,
|
|
||||||
string subject,
|
string subject,
|
||||||
string documentSeries,
|
string documentSeries,
|
||||||
string siaCode,
|
string siaCode,
|
||||||
CancellationToken cancellationToken = default)
|
CancellationToken cancellationToken = default)
|
||||||
=> PostAsync<GestionaCreateFileResponse>(
|
=> PostAsync<GestionaCreateFileResponse>(
|
||||||
"api/gestiona/files",
|
"api/gestiona/files",
|
||||||
new GestionaCreateFileRequest(procedureId, subject, documentSeries, siaCode),
|
new GestionaCreateFileRequest(subject, documentSeries, siaCode),
|
||||||
cancellationToken);
|
cancellationToken);
|
||||||
|
|
||||||
public Task OpenGestionaFileAsync(
|
public Task OpenGestionaFileAsync(
|
||||||
string fileUrl,
|
string fileUrl,
|
||||||
string? fileOpenUrl,
|
string? fileOpenUrl,
|
||||||
Guid managementUnitGroupId,
|
string assignedGroupCode,
|
||||||
Guid assignedGroupId,
|
|
||||||
bool confidential,
|
bool confidential,
|
||||||
string freeTitle,
|
string freeTitle,
|
||||||
string siaCode,
|
|
||||||
CancellationToken cancellationToken = default)
|
CancellationToken cancellationToken = default)
|
||||||
=> PostAsync(
|
=> PostAsync(
|
||||||
"api/gestiona/files/open",
|
"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);
|
cancellationToken);
|
||||||
|
|
||||||
public Task<GestionaExpedienteInfo?> GetGestionaExpedienteAsync(
|
public Task<GestionaExpedienteInfo?> GetGestionaExpedienteAsync(
|
||||||
@@ -149,11 +138,27 @@ public sealed class ApiDenunciasClient
|
|||||||
cancellationToken,
|
cancellationToken,
|
||||||
allowNull: true);
|
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,
|
string fileUrl,
|
||||||
ThirdPartyIdentityData thirdParty,
|
ThirdPartyIdentityData thirdParty,
|
||||||
CancellationToken cancellationToken = default)
|
CancellationToken cancellationToken = default)
|
||||||
=> PostAsync(
|
=> PostAsync<GestionaEnsureThirdResponse>(
|
||||||
"api/gestiona/thirds/ensure-link",
|
"api/gestiona/thirds/ensure-link",
|
||||||
new GestionaEnsureThirdRequest(fileUrl, thirdParty),
|
new GestionaEnsureThirdRequest(fileUrl, thirdParty),
|
||||||
cancellationToken);
|
cancellationToken);
|
||||||
@@ -183,12 +188,13 @@ public sealed class ApiDenunciasClient
|
|||||||
|
|
||||||
public Task TramitarGestionaDocumentAsync(
|
public Task TramitarGestionaDocumentAsync(
|
||||||
string documentUrl,
|
string documentUrl,
|
||||||
string assignedGroupHref,
|
string assignedGroupCode,
|
||||||
int? complaintId,
|
int? complaintId,
|
||||||
|
bool isUpdate = false,
|
||||||
CancellationToken cancellationToken = default)
|
CancellationToken cancellationToken = default)
|
||||||
=> PostAsync(
|
=> PostAsync(
|
||||||
"api/gestiona/documents/tramitar",
|
"api/gestiona/documents/tramitar",
|
||||||
new GestionaTramitarDocumentoRequest(documentUrl, assignedGroupHref, complaintId),
|
new GestionaTramitarDocumentoRequest(documentUrl, assignedGroupCode, complaintId, isUpdate),
|
||||||
cancellationToken);
|
cancellationToken);
|
||||||
|
|
||||||
public Task<List<ExpedienteTerceroDto>> GetGestionaExpedientesPorTerceroAsync(
|
public Task<List<ExpedienteTerceroDto>> GetGestionaExpedientesPorTerceroAsync(
|
||||||
@@ -292,13 +298,16 @@ public sealed class ApiDenunciasClient
|
|||||||
using var response = await client.SendAsync(request, cancellationToken);
|
using var response = await client.SendAsync(request, cancellationToken);
|
||||||
if (response.StatusCode == HttpStatusCode.Unauthorized)
|
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(authorize
|
||||||
throw new UnauthorizedAccessException(message);
|
? "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)
|
if (!response.IsSuccessStatusCode)
|
||||||
|
|||||||
@@ -33,6 +33,16 @@ public sealed class ApiInboxTrackingService : IInboxTrackingService
|
|||||||
new MarkReportImportedRequest(username, report, complaintId),
|
new MarkReportImportedRequest(username, report, complaintId),
|
||||||
cancellationToken);
|
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(
|
public Task EnsureReportCanBeImportedByUserAsync(
|
||||||
string username,
|
string username,
|
||||||
ReportDto report,
|
ReportDto report,
|
||||||
|
|||||||
@@ -141,6 +141,13 @@ pre {
|
|||||||
gap: 0.85rem;
|
gap: 0.85rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.app-status-pills {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
justify-content: flex-end;
|
||||||
|
gap: 0.65rem;
|
||||||
|
}
|
||||||
|
|
||||||
.app-session-pill {
|
.app-session-pill {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -160,6 +167,24 @@ pre {
|
|||||||
box-shadow: 0 0 0 0.2rem rgba(31, 122, 85, 0.12);
|
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 {
|
.app-user-chip {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -548,6 +573,14 @@ h1:focus {
|
|||||||
align-items: stretch;
|
align-items: stretch;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.app-status-pills {
|
||||||
|
justify-content: stretch;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-status-pills .app-session-pill {
|
||||||
|
flex: 1 1 14rem;
|
||||||
|
}
|
||||||
|
|
||||||
.app-user-chip,
|
.app-user-chip,
|
||||||
.app-session-pill {
|
.app-session-pill {
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
|||||||
Reference in New Issue
Block a user