- Añade GET /api/denuncias/{id}/gestiona-fields para exponer los campos diarios requeridos por la integración con Gestiona.
- Devuelve un DTO cerrado con fecha, canal, resumen, datos de hechos, protección, sexo y preferencias de notificación.
- Adapta el login contra GlobalLeaks al nuevo flujo DPoP exigido desde la versión 5.0.94.
- Genera proof DPoP con clave EC P-256 efímera y lo envía en la cabecera DPoP junto a X-Token.
- Mejora el mensaje de error cuando GlobalLeaks rechaza el proof DPoP.
1403 lines
60 KiB
Plaintext
1403 lines
60 KiB
Plaintext
@page "/Actualizaciones"
|
||
@rendermode InteractiveServer
|
||
@attribute [Authorize]
|
||
@using GestionaDenuncias.Shared.Models
|
||
@using System.Globalization
|
||
@using System.IO
|
||
@using System.Linq
|
||
@using System.Text
|
||
|
||
@using GestionaDenuncias.Shared.Helpers
|
||
@using GestionaDenunciasAN.Services
|
||
@attribute [StreamRendering]
|
||
@inject GestionaDenunciasAN.Models.UserState userState
|
||
@inject NavigationManager Navigation
|
||
@inject IHostEnvironment HostEnvironment
|
||
@inject IDenunciaStore DenunciaStore
|
||
@inject ApiDenunciasClient ApiDenuncias
|
||
@inject UiBusyService Busy
|
||
|
||
<PageTitle>Actualizaciones</PageTitle>
|
||
|
||
<style>
|
||
.pendientes-list {
|
||
margin-top: 20px;
|
||
}
|
||
|
||
.collapse-card {
|
||
margin-bottom: 1rem;
|
||
border: 1px solid #ccc;
|
||
border-radius: 6px;
|
||
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||
overflow: hidden;
|
||
transition: transform 0.3s ease, box-shadow 0.3s ease;
|
||
background-color: #fff;
|
||
}
|
||
|
||
.collapse-card:hover {
|
||
transform: scale(1.02);
|
||
box-shadow: 0 4px 8px rgba(0,0,0,0.2);
|
||
}
|
||
|
||
.collapse-card .card-header {
|
||
padding: 0.75rem 1.25rem;
|
||
cursor: pointer;
|
||
background-color: #f7f7f7;
|
||
transition: background-color 0.3s ease;
|
||
}
|
||
|
||
.collapse-card .card-header:hover {
|
||
background-color: #e2e2e2;
|
||
}
|
||
|
||
.section-heading {
|
||
text-align: center;
|
||
font-weight: bold;
|
||
text-decoration: underline;
|
||
font-size: 1.1em;
|
||
margin: 1rem 0 0.5rem;
|
||
}
|
||
|
||
/* Tarjetas de actualización (azules) */
|
||
.collapse-card.update-card {
|
||
background-color: #e3f2fd;
|
||
}
|
||
|
||
.collapse-card.update-card > .card-header {
|
||
background-color: #bbdefb;
|
||
}
|
||
|
||
.seleccionar-col {
|
||
width: 50px;
|
||
text-align: center;
|
||
vertical-align: middle;
|
||
}
|
||
|
||
/* === Estética de modal igual que en Pendientes === */
|
||
|
||
.custom-modal {
|
||
background: rgba(0, 0, 0, 0.5);
|
||
}
|
||
|
||
.custom-modal-dialog {
|
||
max-width: 500px;
|
||
margin: 2rem auto;
|
||
}
|
||
|
||
.custom-modal-content {
|
||
border-radius: 0.5rem;
|
||
box-shadow: 0 5px 15px rgba(0,0,0,0.5);
|
||
border: none;
|
||
}
|
||
|
||
.custom-modal-header {
|
||
background-color: #007bff;
|
||
color: white;
|
||
border-top-left-radius: 0.5rem;
|
||
border-top-right-radius: 0.5rem;
|
||
padding: 1rem;
|
||
}
|
||
|
||
.custom-modal-body {
|
||
padding: 1.5rem;
|
||
background-color: #f8f9fa;
|
||
}
|
||
|
||
.custom-modal-footer {
|
||
border-top: none;
|
||
padding: 1rem;
|
||
}
|
||
|
||
.modal-section-heading {
|
||
font-weight: bold;
|
||
font-size: 1.1em;
|
||
margin-bottom: 0.5rem;
|
||
border-bottom: 2px solid #007bff;
|
||
padding-bottom: 0.25rem;
|
||
}
|
||
</style>
|
||
|
||
<h3>Actualizaciones</h3>
|
||
|
||
@if (!string.IsNullOrWhiteSpace(loadError))
|
||
{
|
||
<div class="alert alert-danger">@loadError</div>
|
||
}
|
||
@if (!string.IsNullOrWhiteSpace(operationNotice))
|
||
{
|
||
<div class="alert alert-warning">@operationNotice</div>
|
||
}
|
||
|
||
<input type="text"
|
||
class="form-control"
|
||
placeholder="Buscar actualizaciones..."
|
||
@bind="busqueda"
|
||
@bind:event="oninput"
|
||
style="margin-bottom:20px;" />
|
||
|
||
@if (!hasLoaded)
|
||
{
|
||
<div class="alert alert-info">Cargando datos...</div>
|
||
}
|
||
else if (actualizaciones == null || !actualizaciones.Any())
|
||
{
|
||
<div class="alert alert-secondary">No hay actualizaciones pendientes.</div>
|
||
}
|
||
else
|
||
{
|
||
<div class="pendientes-list">
|
||
@foreach (var denuncia in actualizaciones.Where(d =>
|
||
string.IsNullOrWhiteSpace(busqueda) ||
|
||
d.Id_Denuncia.ToString().Contains(busqueda) ||
|
||
(!string.IsNullOrEmpty(d.Asunto) && d.Asunto.Contains(busqueda, StringComparison.OrdinalIgnoreCase)) ||
|
||
(!string.IsNullOrEmpty(d.Nombre) && d.Nombre.Contains(busqueda, StringComparison.OrdinalIgnoreCase)) ||
|
||
(!string.IsNullOrEmpty(d.Apellidos) && d.Apellidos.Contains(busqueda, StringComparison.OrdinalIgnoreCase)) ||
|
||
(!string.IsNullOrEmpty(d.RazonSocial) && d.RazonSocial.Contains(busqueda, StringComparison.OrdinalIgnoreCase))
|
||
))
|
||
{
|
||
var collapseId = $"upd{denuncia.Id_Denuncia}";
|
||
<div class="card mb-2 collapse-card update-card">
|
||
<div class="card-header d-flex justify-content-between align-items-center"
|
||
data-bs-toggle="collapse"
|
||
data-bs-target="#@collapseId"
|
||
aria-expanded="false"
|
||
aria-controls="@collapseId">
|
||
<h5 class="mb-0">Denuncia ID: @denuncia.Id_Denuncia (Actualización)</h5>
|
||
|
||
<div class="d-flex align-items-center">
|
||
<div class="text-muted small me-3">
|
||
@if (denuncia.FechaSubidaAGestiona != DateTime.MinValue)
|
||
{
|
||
<span>Procesada: @denuncia.FechaSubidaAGestiona.ToString("dd/MM/yyyy")</span>
|
||
}
|
||
else if (denuncia.Fecha != DateTime.MinValue)
|
||
{
|
||
<span>Creada: @denuncia.Fecha.ToString("dd/MM/yyyy")</span>
|
||
}
|
||
</div>
|
||
|
||
<button type="button"
|
||
class="btn btn-success btn-sm"
|
||
@onclick:stopPropagation="true"
|
||
@onclick="() => OpenEnviarAGestionaModal(denuncia)">
|
||
Configurar actualizacion expediente
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
<div id="@collapseId" class="collapse">
|
||
<div class="card-body">
|
||
<h5 class="section-heading">Datos Generales</h5>
|
||
<dl class="row">
|
||
@if (denuncia.Id_RegistroDenuncia != 0)
|
||
{
|
||
<dt class="col-sm-3">ID Registro</dt>
|
||
<dd class="col-sm-9">@denuncia.Id_RegistroDenuncia</dd>
|
||
}
|
||
@if (!string.IsNullOrWhiteSpace(denuncia.ExpedienteGestionaMostrable))
|
||
{
|
||
<dt class="col-sm-3">Nº expediente Gestiona</dt>
|
||
<dd class="col-sm-9">@denuncia.ExpedienteGestionaMostrable</dd>
|
||
}
|
||
@if (!string.IsNullOrWhiteSpace(denuncia.Etiqueta))
|
||
{
|
||
<dt class="col-sm-3">Etiqueta</dt>
|
||
<dd class="col-sm-9">@denuncia.Etiqueta</dd>
|
||
}
|
||
@if (!string.IsNullOrWhiteSpace(denuncia.Estado))
|
||
{
|
||
<dt class="col-sm-3">Estado</dt>
|
||
<dd class="col-sm-9">@denuncia.Estado</dd>
|
||
}
|
||
@if (!string.IsNullOrWhiteSpace(denuncia.Tipo_Denuncia))
|
||
{
|
||
<dt class="col-sm-3">Tipo</dt>
|
||
<dd class="col-sm-9">@denuncia.Tipo_Denuncia</dd>
|
||
}
|
||
</dl>
|
||
|
||
@if (IsExternalGestionaUpdate(denuncia))
|
||
{
|
||
<div class="alert alert-warning" role="alert">
|
||
@GetExternalUpdateWarningText()
|
||
</div>
|
||
}
|
||
|
||
@if (!string.IsNullOrWhiteSpace(denuncia.NombreResuelto) ||
|
||
!string.IsNullOrWhiteSpace(denuncia.ApellidosResueltos) ||
|
||
!string.IsNullOrWhiteSpace(denuncia.RazonSocialResuelta) ||
|
||
!string.IsNullOrWhiteSpace(denuncia.DocumentoResuelto))
|
||
{
|
||
<h5 class="section-heading">Denunciante</h5>
|
||
<dl class="row">
|
||
@if (!string.IsNullOrWhiteSpace(denuncia.TipoDenunciante))
|
||
{
|
||
<dt class="col-sm-3">Tipo de denunciante</dt>
|
||
<dd class="col-sm-9">@denuncia.TipoDenunciante</dd>
|
||
}
|
||
@if (!string.IsNullOrWhiteSpace(denuncia.RazonSocialResuelta))
|
||
{
|
||
<dt class="col-sm-3">Razón social</dt>
|
||
<dd class="col-sm-9">@denuncia.RazonSocialResuelta</dd>
|
||
}
|
||
@if (!string.IsNullOrWhiteSpace(denuncia.NombreResuelto))
|
||
{
|
||
<dt class="col-sm-3">Nombre</dt>
|
||
<dd class="col-sm-9">@denuncia.NombreResuelto</dd>
|
||
}
|
||
@if (!string.IsNullOrWhiteSpace(denuncia.PrimerApellidoResuelto))
|
||
{
|
||
<dt class="col-sm-3">1º apellido</dt>
|
||
<dd class="col-sm-9">@denuncia.PrimerApellidoResuelto</dd>
|
||
}
|
||
@if (!string.IsNullOrWhiteSpace(denuncia.SegundoApellidoResuelto))
|
||
{
|
||
<dt class="col-sm-3">2º apellido</dt>
|
||
<dd class="col-sm-9">@denuncia.SegundoApellidoResuelto</dd>
|
||
}
|
||
@if (!string.IsNullOrWhiteSpace(denuncia.ApellidosResueltos))
|
||
{
|
||
<dt class="col-sm-3">Apellidos</dt>
|
||
<dd class="col-sm-9">@denuncia.ApellidosResueltos</dd>
|
||
}
|
||
@if (!string.IsNullOrWhiteSpace(denuncia.DocumentoResuelto))
|
||
{
|
||
<dt class="col-sm-3">@GetDocumentoLabel(denuncia)</dt>
|
||
<dd class="col-sm-9">@denuncia.DocumentoResuelto</dd>
|
||
}
|
||
</dl>
|
||
}
|
||
|
||
<h5 class="section-heading">Detalles</h5>
|
||
<dl class="row">
|
||
<dt class="col-sm-3">Asunto</dt>
|
||
<dd class="col-sm-9">@denuncia.Asunto</dd>
|
||
<dt class="col-sm-3">A Quién</dt>
|
||
<dd class="col-sm-9">@denuncia.A_Quien_Denuncia</dd>
|
||
@if (!string.IsNullOrWhiteSpace(denuncia.DenunciadoDetalle))
|
||
{
|
||
<dt class="col-sm-3">Detalle denunciado</dt>
|
||
<dd class="col-sm-9">@denuncia.DenunciadoDetalle</dd>
|
||
}
|
||
<dt class="col-sm-3">Descripción</dt>
|
||
<dd class="col-sm-9">@denuncia.Descripcion_Denuncia</dd>
|
||
@if (!string.IsNullOrWhiteSpace(denuncia.OrganismoDenunciado))
|
||
{
|
||
<dt class="col-sm-3">Organismo previo</dt>
|
||
<dd class="col-sm-9">@denuncia.OrganismoDenunciado</dd>
|
||
}
|
||
@if (!string.IsNullOrWhiteSpace(denuncia.SolicitaProteccion))
|
||
{
|
||
<dt class="col-sm-3">Solicita protección</dt>
|
||
<dd class="col-sm-9">@denuncia.SolicitaProteccion</dd>
|
||
}
|
||
@if (!string.IsNullOrWhiteSpace(denuncia.MedidasProteccionSolicitadas))
|
||
{
|
||
<dt class="col-sm-3">Medidas solicitadas</dt>
|
||
<dd class="col-sm-9">@denuncia.MedidasProteccionSolicitadas</dd>
|
||
}
|
||
<dt class="col-sm-3">Lugar</dt>
|
||
<dd class="col-sm-9">@denuncia.Lugar_Hechos</dd>
|
||
@if (denuncia.Fecha_Hechos != DateTime.MinValue)
|
||
{
|
||
<dt class="col-sm-3">Fecha Hechos</dt>
|
||
<dd class="col-sm-9">@denuncia.Fecha_Hechos.ToString("dd/MM/yyyy")</dd>
|
||
}
|
||
@if (!string.IsNullOrWhiteSpace(denuncia.AutorizaRemision))
|
||
{
|
||
<dt class="col-sm-3">Autoriza remisión</dt>
|
||
<dd class="col-sm-9">@denuncia.AutorizaRemision</dd>
|
||
}
|
||
</dl>
|
||
|
||
@{
|
||
var camposFormulario = denuncia.GetCamposFormulario();
|
||
}
|
||
@if (camposFormulario.Count > 0)
|
||
{
|
||
<h5 class="section-heading">Formulario Original</h5>
|
||
@foreach (var grupoCampos in camposFormulario.GroupBy(field => string.IsNullOrWhiteSpace(field.Section) ? "Sin sección" : field.Section))
|
||
{
|
||
<h6 class="mt-3">@grupoCampos.Key</h6>
|
||
<dl class="row">
|
||
@foreach (var campo in grupoCampos)
|
||
{
|
||
<dt class="col-sm-4">@campo.Label</dt>
|
||
<dd class="col-sm-8">@(string.IsNullOrWhiteSpace(campo.Value) ? "—" : campo.Value)</dd>
|
||
}
|
||
</dl>
|
||
}
|
||
}
|
||
|
||
@if (ficherosAdjuntos.TryGetValue(denuncia.Id_Denuncia, out var fAdj) && fAdj.Any())
|
||
{
|
||
<h5 class="section-heading">Ficheros pendientes de subir</h5>
|
||
<table class="table table-striped">
|
||
<thead>
|
||
<tr>
|
||
<th>Subir</th>
|
||
<th>Nombre</th>
|
||
<th>Fecha</th>
|
||
<th>Motivo</th>
|
||
<th>Tamaño (bytes)</th>
|
||
<th>Ver</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
@foreach (var f in fAdj)
|
||
{
|
||
var isReport = IsReportFileName(f.NombreFichero);
|
||
<tr>
|
||
<td>
|
||
<input class="form-check-input"
|
||
type="checkbox"
|
||
checked="@IsFileSelectedForUpload(denuncia.Id_Denuncia, f.NombreFichero)"
|
||
disabled="@isReport"
|
||
title='@(isReport ? "El report se sube siempre" : "Incluir este fichero en la subida")'
|
||
@onchange="args => ToggleFileSelection(denuncia.Id_Denuncia, f.NombreFichero, args.Value is bool selected && selected)" />
|
||
</td>
|
||
<td>
|
||
@f.NombreFichero
|
||
@if (isReport)
|
||
{
|
||
<span class="badge bg-primary ms-2">Obligatorio</span>
|
||
}
|
||
</td>
|
||
<td>@FormatFileDate(f.Fecha)</td>
|
||
<td>
|
||
@if (f.EsReport)
|
||
{
|
||
<span class="badge bg-primary">Siempre se sube para notificar</span>
|
||
}
|
||
else if (IsExternalGestionaUpdate(denuncia))
|
||
{
|
||
<span class="badge bg-warning text-dark">@GetExternalUpdateFileReason()</span>
|
||
}
|
||
else
|
||
{
|
||
<span class="badge bg-warning text-dark">Hash nuevo pendiente</span>
|
||
}
|
||
</td>
|
||
<td>@f.Fichero?.Length</td>
|
||
<td>
|
||
@if (f.Fichero != null && f.Fichero.Length > 0)
|
||
{
|
||
<a class="btn btn-primary btn-sm" href="@BuildAttachmentContentUrl(denuncia.Id_Denuncia, f.NombreFichero)" target="_blank" rel="noopener">
|
||
<i class="bi bi-eye"></i> Ver
|
||
</a>
|
||
}
|
||
else
|
||
{
|
||
<span class="text-muted">—</span>
|
||
}
|
||
</td>
|
||
</tr>
|
||
}
|
||
</tbody>
|
||
</table>
|
||
}
|
||
else
|
||
{
|
||
<div class="alert alert-light">Sin ficheros pendientes.</div>
|
||
}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
}
|
||
</div>
|
||
}
|
||
|
||
<script>
|
||
function createObjectUrl(base64, contentType) {
|
||
const binaryString = window.atob(base64);
|
||
const len = binaryString.length;
|
||
const bytes = new Uint8Array(len);
|
||
for (let i = 0; i < len; i++) { bytes[i] = binaryString.charCodeAt(i); }
|
||
const blob = new Blob([bytes], { type: contentType });
|
||
return URL.createObjectURL(blob);
|
||
}
|
||
function openFile(event, base64, contentType) {
|
||
event.preventDefault();
|
||
event.stopPropagation();
|
||
const objectUrl = createObjectUrl(base64, contentType);
|
||
window.open(objectUrl, '_blank');
|
||
}
|
||
</script>
|
||
|
||
@if (showModal && selectedDenuncias != null)
|
||
{
|
||
<div class="modal custom-modal fade show update-modal" style="display:block;" tabindex="-1">
|
||
<div class="modal-dialog custom-modal-dialog">
|
||
<div class="modal-content custom-modal-content">
|
||
<div class="modal-header custom-modal-header">
|
||
<h5 class="modal-title">Enviar a Gestiona</h5>
|
||
<button type="button" class="btn-close" @onclick="CloseModal"></button>
|
||
</div>
|
||
|
||
<div class="modal-body custom-modal-body">
|
||
@if (autoSearchLoading)
|
||
{
|
||
<div class="alert alert-info d-flex align-items-center" role="alert">
|
||
<div class="spinner-border spinner-border-sm me-2" role="status" aria-hidden="true"></div>
|
||
Buscando expediente existente en Gestiona…
|
||
</div>
|
||
}
|
||
else if (autoSearchTried && !string.IsNullOrWhiteSpace(autoFoundFileUrl))
|
||
{
|
||
<div class="alert alert-success" role="alert">
|
||
<div class="fw-semibold">Expediente detectado en Gestiona.</div>
|
||
<div>Asunto: <strong>@(autoFoundTitle ?? "(sin título)")</strong></div>
|
||
<div class="text-muted small">@autoFoundFileUrl</div>
|
||
<div class="form-check mt-2">
|
||
<input class="form-check-input" type="checkbox" id="chkUsarDetectado" @bind="useAutoFoundExpediente" />
|
||
<label class="form-check-label" for="chkUsarDetectado">
|
||
Añadir documentos a este expediente.
|
||
</label>
|
||
</div>
|
||
</div>
|
||
}
|
||
else if (autoSearchTried && string.IsNullOrWhiteSpace(autoFoundFileUrl) && string.IsNullOrWhiteSpace(selectedDenuncias!.Expediente_Gestiona))
|
||
{
|
||
<div class="alert alert-warning" role="alert">
|
||
<div class="fw-semibold">No se ha detectado expediente en Gestiona por asunto.</div>
|
||
<div class="small">Puede que esto no sea una actualización del mismo caso.</div>
|
||
</div>
|
||
}
|
||
|
||
@if (!string.IsNullOrWhiteSpace(operationError))
|
||
{
|
||
<div class="alert alert-danger" role="alert">
|
||
@operationError
|
||
</div>
|
||
}
|
||
|
||
@if (IsExternalGestionaUpdate(selectedDenuncias))
|
||
{
|
||
<div class="alert alert-warning" role="alert">
|
||
@GetExternalUpdateWarningText()
|
||
</div>
|
||
}
|
||
|
||
@if (!string.IsNullOrWhiteSpace(selectedDenuncias.ExpedienteGestionaMostrable))
|
||
{
|
||
<div class="alert alert-info" role="alert">
|
||
<div class="fw-semibold">Expediente de Gestiona donde se hará la actualización</div>
|
||
<div>Nº expediente: <strong>@selectedDenuncias.ExpedienteGestionaMostrable</strong></div>
|
||
</div>
|
||
}
|
||
|
||
<h6 class="modal-section-heading">Descripción</h6>
|
||
<div class="mb-3">
|
||
<input type="text" class="form-control" @bind="nuevoAsunto" readonly />
|
||
</div>
|
||
|
||
<h6 class="modal-section-heading">Nombre de los documentos</h6>
|
||
<div class="mb-3">
|
||
<input type="text" class="form-control" @bind="nombreDocumentos"
|
||
placeholder="Ej.: Gestión AN (Documento Adjunto 1 Gestión AN...)" />
|
||
<small class="text-muted">
|
||
Se aplica al modo individual. <em>report.txt</em> se sube como <strong>Denuncia</strong> si entra en esta actualización.
|
||
</small>
|
||
</div>
|
||
|
||
<h6 class="modal-section-heading">Modo de subida</h6>
|
||
<div class="form-check">
|
||
<input class="form-check-input" type="radio" name="uploadMode" id="modoMerge"
|
||
checked='@(uploadMode == "merge")' @onclick='() => uploadMode = "merge"' />
|
||
<label class="form-check-label" for="modoMerge">Unir todos los ficheros en un único PDF</label>
|
||
</div>
|
||
<div class="form-check">
|
||
<input class="form-check-input" type="radio" name="uploadMode" id="modoIndividual"
|
||
checked='@(uploadMode == "individual")' @onclick='() => uploadMode = "individual"' />
|
||
<label class="form-check-label" for="modoIndividual">Subir ficheros de forma independiente</label>
|
||
</div>
|
||
|
||
<h6 class="modal-section-heading">Grupo de destino</h6>
|
||
<div class="form-check">
|
||
<input class="form-check-input" type="radio" name="selectedGroup" id="grupo600"
|
||
checked='@(selectedGroup == "600")' @onclick='() => selectedGroup = "600"' />
|
||
<label class="form-check-label" for="grupo600">
|
||
600. Asuntos Jurídicos y Protección a la Persona Denunciante
|
||
</label>
|
||
</div>
|
||
<div class="form-check">
|
||
<input class="form-check-input" type="radio" name="selectedGroup" id="grupo510"
|
||
checked='@(selectedGroup == "510")' @onclick='() => selectedGroup = "510"' />
|
||
<label class="form-check-label" for="grupo510">510. SDI – Investigación Entradas</label>
|
||
</div>
|
||
|
||
@{
|
||
var modalThirdParty = selectedThirdParty ?? ThirdPartyIdentityData.FromComplaint(selectedDenuncias);
|
||
}
|
||
<h6 class="modal-section-heading mt-3">Tercero (denunciante)</h6>
|
||
|
||
<div class="alert alert-light border mb-3">
|
||
Los datos del tercero se cargan automáticamente desde la denuncia y no se pueden editar aquí.
|
||
</div>
|
||
|
||
@if (modalThirdParty.IsAnonymous)
|
||
{
|
||
<div class="alert alert-warning mb-3">
|
||
Denuncia anónima. Se enlazará automáticamente el tercero <strong>00000000T</strong>.
|
||
</div>
|
||
}
|
||
|
||
<div class="row g-2">
|
||
<div class="col-6 mb-2">
|
||
<label class="form-label">@GetDocumentoLabel(selectedDenuncias)</label>
|
||
<input class="form-control" value="@GetReadOnlyValue(modalThirdParty.DocumentId)" readonly />
|
||
</div>
|
||
<div class="col-6 mb-2">
|
||
<label class="form-label">Email</label>
|
||
<input class="form-control" value="@GetReadOnlyValue(modalThirdParty.Email)" readonly />
|
||
</div>
|
||
</div>
|
||
|
||
@if (!string.IsNullOrWhiteSpace(selectedDenuncias.TipoDenunciante))
|
||
{
|
||
<div class="row g-2">
|
||
<div class="col-12 mb-2">
|
||
<label class="form-label">Tipo de denunciante</label>
|
||
<input class="form-control" value="@selectedDenuncias.TipoDenunciante" readonly />
|
||
</div>
|
||
</div>
|
||
}
|
||
|
||
@if (modalThirdParty.IsLegalEntity)
|
||
{
|
||
<div class="row g-2">
|
||
<div class="col-12 mb-2">
|
||
<label class="form-label">Razón social</label>
|
||
<input class="form-control" value="@GetReadOnlyValue(selectedDenuncias.RazonSocialResuelta)" readonly />
|
||
</div>
|
||
</div>
|
||
}
|
||
else
|
||
{
|
||
<div class="row g-2">
|
||
<div class="col-4 mb-2">
|
||
<label class="form-label">Nombre</label>
|
||
<input class="form-control" value="@GetReadOnlyValue(selectedDenuncias.NombreResuelto)" readonly />
|
||
</div>
|
||
<div class="col-4 mb-2">
|
||
<label class="form-label">1º apellido</label>
|
||
<input class="form-control" value="@GetReadOnlyValue(selectedDenuncias.PrimerApellidoResuelto)" readonly />
|
||
</div>
|
||
<div class="col-4 mb-2">
|
||
<label class="form-label">2º apellido</label>
|
||
<input class="form-control" value="@GetReadOnlyValue(selectedDenuncias.SegundoApellidoResuelto)" readonly />
|
||
</div>
|
||
</div>
|
||
}
|
||
|
||
@if (!modalThirdParty.IsAnonymous && HasPostalAddress(selectedDenuncias))
|
||
{
|
||
<div class="row g-2">
|
||
<div class="col-12 mb-2">
|
||
<label class="form-label">Dirección postal</label>
|
||
<textarea class="form-control" rows="2" readonly>@BuildPostalAddressSummary(selectedDenuncias)</textarea>
|
||
</div>
|
||
</div>
|
||
}
|
||
|
||
<small class="text-muted">
|
||
Antes de subir la actualización se comprobará el tercero extraído del formulario y, si no está enlazado al expediente, se enlazará.
|
||
</small>
|
||
</div>
|
||
|
||
<div class="modal-footer custom-modal-footer">
|
||
<button type="button" class="btn btn-secondary" @onclick="CloseModal">Cancelar</button>
|
||
<button type="button" class="btn btn-primary" disabled="@isUploading" @onclick="ConfirmarEnvio">Confirmar</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="modal-backdrop fade show"></div>
|
||
}
|
||
|
||
@code {
|
||
private bool hasLoaded = false;
|
||
private string busqueda = "";
|
||
private List<DenunciasGestiona> actualizaciones = new();
|
||
private Dictionary<int, List<FicherosDenuncias>> ficherosAdjuntos = new();
|
||
private Dictionary<int, HashSet<string>> excludedUploadFiles = new();
|
||
private string loadError = string.Empty;
|
||
private string operationError = string.Empty;
|
||
private string operationNotice = string.Empty;
|
||
private DateOnly? externalUpdateCutoffDate;
|
||
|
||
// --- Modal / estado ---
|
||
private bool showModal = false;
|
||
private bool isUploading = false;
|
||
private string uploadMode = "individual";
|
||
private string selectedGroup = "600";
|
||
private string nuevoAsunto = "";
|
||
private string nombreDocumentos = "";
|
||
private DenunciasGestiona? selectedDenuncias;
|
||
private ThirdPartyIdentityData? selectedThirdParty;
|
||
|
||
// --- Detección automática en Gestiona por asunto ---
|
||
private bool autoSearchLoading = false;
|
||
private bool autoSearchTried = false;
|
||
private string? autoFoundFileUrl = null;
|
||
private string? autoFoundTitle = null;
|
||
private bool useAutoFoundExpediente = true;
|
||
|
||
protected override async Task OnAfterRenderAsync(bool firstRender)
|
||
{
|
||
if (!firstRender) return;
|
||
|
||
await CargarDatosAsync();
|
||
}
|
||
|
||
private async Task CargarDatosAsync()
|
||
{
|
||
try
|
||
{
|
||
loadError = string.Empty;
|
||
var config = await ApiDenuncias.GetAppConfigurationAsync();
|
||
externalUpdateCutoffDate = ParseConfiguredCutoffDate(config.ExternalUpdateCutoffDate);
|
||
var todas = await CargarDenunciasJsonAsync();
|
||
actualizaciones = todas
|
||
.Where(d => d.EsActualizacion)
|
||
.OrderByDescending(d => d.FechaSubidaAGestiona != DateTime.MinValue ? d.FechaSubidaAGestiona : d.Fecha)
|
||
.ToList();
|
||
|
||
ficherosAdjuntos = await CargarFicherosPorDenunciaAsync(actualizaciones);
|
||
actualizaciones = actualizaciones
|
||
.Where(d => ficherosAdjuntos.ContainsKey(d.Id_Denuncia))
|
||
.ToList();
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
actualizaciones.Clear();
|
||
ficherosAdjuntos.Clear();
|
||
loadError = $"No se han podido cargar las actualizaciones: {ex.Message}";
|
||
}
|
||
|
||
hasLoaded = true;
|
||
StateHasChanged();
|
||
}
|
||
|
||
private async Task<List<DenunciasGestiona>> CargarDenunciasJsonAsync()
|
||
{
|
||
return await DenunciaStore.GetDenunciasByScopeAsync(DenunciaListScope.Updates);
|
||
}
|
||
|
||
private async Task<Dictionary<int, List<FicherosDenuncias>>> CargarFicherosPorDenunciaAsync(IEnumerable<DenunciasGestiona> denuncias)
|
||
{
|
||
var result = new Dictionary<int, List<FicherosDenuncias>>();
|
||
foreach (var denuncia in denuncias.Where(d => d.Id_Denuncia > 0).DistinctBy(d => d.Id_Denuncia))
|
||
{
|
||
var ficheros = GetPendingUpdateFilesForComplaint(
|
||
denuncia,
|
||
await DenunciaStore.GetFicherosByDenunciaAsync(denuncia.Id_Denuncia));
|
||
if (ficheros.Count > 0)
|
||
{
|
||
result[denuncia.Id_Denuncia] = ficheros;
|
||
}
|
||
}
|
||
|
||
return result;
|
||
}
|
||
|
||
private async Task OpenEnviarAGestionaModal(DenunciasGestiona d)
|
||
{
|
||
selectedDenuncias = d;
|
||
nuevoAsunto = string.IsNullOrWhiteSpace(d.NombreDenuncia) ? $"Denuncia-{d.Id_Denuncia}-CD" : d.NombreDenuncia;
|
||
nombreDocumentos = "";
|
||
selectedThirdParty = ThirdPartyIdentityData.FromComplaint(d);
|
||
selectedGroup = NormalizeUpdateGroup(selectedGroup);
|
||
operationError = string.Empty;
|
||
operationNotice = string.Empty;
|
||
|
||
autoFoundFileUrl = null;
|
||
autoFoundTitle = null;
|
||
useAutoFoundExpediente = true;
|
||
autoSearchTried = false;
|
||
|
||
if (!string.IsNullOrWhiteSpace(d.Expediente_Gestiona) &&
|
||
string.IsNullOrWhiteSpace(d.CodigoExpedienteGestiona))
|
||
{
|
||
await SincronizarExpedienteGestionaAsync(d, d.Expediente_Gestiona);
|
||
}
|
||
|
||
if (string.IsNullOrWhiteSpace(d.Expediente_Gestiona))
|
||
{
|
||
autoSearchLoading = true;
|
||
showModal = true;
|
||
StateHasChanged();
|
||
|
||
try
|
||
{
|
||
// Búsqueda automática desactivada temporalmente
|
||
}
|
||
catch
|
||
{
|
||
// no bloquea
|
||
}
|
||
finally
|
||
{
|
||
autoSearchLoading = false;
|
||
autoSearchTried = true;
|
||
StateHasChanged();
|
||
}
|
||
}
|
||
else
|
||
{
|
||
autoFoundFileUrl = null;
|
||
autoFoundTitle = null;
|
||
autoSearchTried = false;
|
||
useAutoFoundExpediente = true;
|
||
showModal = true;
|
||
StateHasChanged();
|
||
}
|
||
}
|
||
|
||
private void CloseModal()
|
||
{
|
||
showModal = false;
|
||
selectedDenuncias = null;
|
||
selectedThirdParty = null;
|
||
nuevoAsunto = "";
|
||
nombreDocumentos = "";
|
||
operationError = string.Empty;
|
||
autoFoundFileUrl = null;
|
||
autoFoundTitle = null;
|
||
autoSearchLoading = false;
|
||
autoSearchTried = false;
|
||
useAutoFoundExpediente = true;
|
||
}
|
||
|
||
private static bool IsReportFileName(string? fileName)
|
||
{
|
||
if (string.IsNullOrWhiteSpace(fileName))
|
||
{
|
||
return false;
|
||
}
|
||
|
||
var name = Path.GetFileNameWithoutExtension(fileName);
|
||
var extension = Path.GetExtension(fileName);
|
||
return name.StartsWith("report", StringComparison.OrdinalIgnoreCase) &&
|
||
(extension.Equals(".txt", StringComparison.OrdinalIgnoreCase) ||
|
||
extension.Equals(".pdf", StringComparison.OrdinalIgnoreCase));
|
||
}
|
||
private string FixFileName(string input)
|
||
{
|
||
var n = input.Normalize(NormalizationForm.FormD);
|
||
var sb = new StringBuilder();
|
||
foreach (var c in n)
|
||
if (CharUnicodeInfo.GetUnicodeCategory(c) != UnicodeCategory.NonSpacingMark) sb.Append(c);
|
||
var clean = sb.ToString().Normalize(NormalizationForm.FormC).Replace(' ', '_');
|
||
return new string(clean.Where(ch => ch <= 127).ToArray());
|
||
}
|
||
|
||
private string BuildMergedAttachmentsFileName(int denunciaId, DateTime timestampUtc)
|
||
{
|
||
var baseName = string.IsNullOrWhiteSpace(nombreDocumentos)
|
||
? $"Adjuntos {denunciaId}_{timestampUtc:yyyyMMddHHmmss}"
|
||
: nombreDocumentos.Trim();
|
||
|
||
return FixFileName($"{Path.GetFileNameWithoutExtension(baseName)}.pdf");
|
||
}
|
||
|
||
private async Task ActualizarDenunciaAsync(DenunciasGestiona d)
|
||
{
|
||
await DenunciaStore.UpsertDenunciaAsync(d);
|
||
}
|
||
|
||
private async Task ConfirmarEnvio()
|
||
{
|
||
if (selectedDenuncias == null) return;
|
||
|
||
if (!selectedDenuncias.EsActualizacion)
|
||
{
|
||
CloseModal();
|
||
Navigation.NavigateTo("/Pendientes");
|
||
return;
|
||
}
|
||
|
||
try
|
||
{
|
||
isUploading = true;
|
||
operationError = string.Empty;
|
||
operationNotice = string.Empty;
|
||
selectedGroup = NormalizeUpdateGroup(selectedGroup);
|
||
using var busy = Busy.Show(
|
||
"Enviando actualizacion",
|
||
"Preparando expediente, carpeta de actualizacion y documentos.");
|
||
StateHasChanged();
|
||
await Task.Yield();
|
||
|
||
// 1) Ficheros a subir
|
||
Busy.Update(message: "Cargando ficheros pendientes de esta actualizacion.", detail: "Paso 1 de 8");
|
||
var existentesF = await DenunciaStore.GetFicherosByDenunciaAsync(selectedDenuncias.Id_Denuncia);
|
||
var fDenuncia = GetPendingUpdateFilesForComplaint(selectedDenuncias, existentesF);
|
||
var ficherosNoSeleccionados = GetExcludedUploadFileNames(selectedDenuncias.Id_Denuncia, fDenuncia);
|
||
var ficherosSeleccionados = GetSelectedUploadFiles(selectedDenuncias.Id_Denuncia, fDenuncia);
|
||
|
||
var todos = new List<(string FileName, byte[] Content)>();
|
||
foreach (var f in ficherosSeleccionados)
|
||
{
|
||
if (f.Fichero == null) continue;
|
||
todos.Add((f.NombreFichero ?? string.Empty, f.Fichero));
|
||
}
|
||
|
||
var ficherosVacios = todos
|
||
.Where(t => t.Content.Length == 0)
|
||
.Select(t => string.IsNullOrWhiteSpace(t.FileName) ? "(sin nombre)" : t.FileName)
|
||
.Distinct(StringComparer.OrdinalIgnoreCase)
|
||
.ToList();
|
||
todos = todos
|
||
.Where(t => t.Content.Length > 0)
|
||
.ToList();
|
||
|
||
if (!todos.Any())
|
||
{
|
||
operationError = ficherosVacios.Count == 0
|
||
? $"La denuncia #{selectedDenuncias.Id_Denuncia} no tiene ficheros pendientes para subir como actualización."
|
||
: $"La denuncia #{selectedDenuncias.Id_Denuncia} solo tiene ficheros vacíos y no se ha subido nada. Ficheros omitidos: {string.Join(", ", ficherosVacios)}.";
|
||
return;
|
||
}
|
||
|
||
// 2) Determinar expediente destino
|
||
var expedienteCreadoEnGestiona = false;
|
||
string fileUrl;
|
||
if (selectedDenuncias.EnGestiona && !string.IsNullOrWhiteSpace(selectedDenuncias.Expediente_Gestiona))
|
||
{
|
||
fileUrl = selectedDenuncias.Expediente_Gestiona!;
|
||
}
|
||
else if (!string.IsNullOrWhiteSpace(autoFoundFileUrl) && useAutoFoundExpediente)
|
||
{
|
||
fileUrl = autoFoundFileUrl!;
|
||
selectedDenuncias.Expediente_Gestiona = fileUrl;
|
||
selectedDenuncias.EnGestiona = true;
|
||
}
|
||
else
|
||
{
|
||
Busy.Update(message: "Creando expediente nuevo en Gestiona.", detail: "Paso 2 de 8");
|
||
var createdFile = await ApiDenuncias.CreateGestionaFileAsync(
|
||
nuevoAsunto,
|
||
"RQ2ZLC - Expediente de Denuncias",
|
||
"3109963"
|
||
);
|
||
fileUrl = createdFile.FileUrl;
|
||
Busy.Update(message: "Abriendo expediente y asignando el grupo elegido.", detail: "Paso 2 de 8");
|
||
await ApiDenuncias.OpenGestionaFileAsync(
|
||
fileUrl,
|
||
createdFile.FileOpenUrl,
|
||
assignedGroupCode: selectedGroup,
|
||
confidential: selectedDenuncias.Confidencial,
|
||
freeTitle: nuevoAsunto
|
||
);
|
||
selectedDenuncias.Expediente_Gestiona = fileUrl;
|
||
selectedDenuncias.EnGestiona = true;
|
||
expedienteCreadoEnGestiona = true;
|
||
}
|
||
|
||
Busy.Update(message: "Guardando referencia local del expediente.", detail: "Paso 3 de 8");
|
||
selectedDenuncias.Expediente_Gestiona = fileUrl;
|
||
selectedDenuncias.EnGestiona = true;
|
||
|
||
Busy.Update(message: "Asignando el expediente al grupo elegido.", detail: "Paso 3 de 8");
|
||
await ApiDenuncias.AssignGestionaFileAsync(fileUrl, selectedGroup);
|
||
|
||
var thirdParty = selectedThirdParty ?? ThirdPartyIdentityData.FromComplaint(selectedDenuncias);
|
||
|
||
Busy.Update(message: "Resolviendo tercero y enlazandolo al expediente.", detail: "Paso 4 de 8");
|
||
var thirdResult = await ApiDenuncias.EnsureGestionaThirdAndLinkAsync(fileUrl, thirdParty);
|
||
|
||
var ahoraUtc = DateTime.UtcNow;
|
||
var carpetaActualizacion = FixFileName($"Actualizacion {DateTime.Now:yyyy-MM-dd HH-mm}");
|
||
Busy.Update(message: "Creando carpeta de actualizacion en Gestiona.", detail: "Paso 5 de 8");
|
||
var carpetaActualizacionGestiona = await ApiDenuncias.CreateGestionaFolderAsync(fileUrl, carpetaActualizacion);
|
||
var documentsTargetUrl = carpetaActualizacionGestiona.DocumentsTargetUrl;
|
||
var nombresOriginalesSubidos = new List<string>();
|
||
var nombresFinalesSubidos = new List<string>();
|
||
string? documentoParaTramitar = null;
|
||
|
||
var report = todos.FirstOrDefault(t =>
|
||
IsReportFileName(t.FileName));
|
||
|
||
if (!string.IsNullOrWhiteSpace(report.FileName))
|
||
{
|
||
Busy.Update(message: "Preparando y subiendo el documento principal.", detail: "Paso 6 de 8");
|
||
var reportPdfBytes = PdfHelper.MergeReportToPdf(
|
||
new (string FileName, byte[] Content)[] { (report.FileName, report.Content) });
|
||
var reportFinalName = FixFileName("Denuncia.pdf");
|
||
documentoParaTramitar = await ApiDenuncias.UploadGestionaDocumentAsync(
|
||
documentsTargetUrl,
|
||
reportPdfBytes,
|
||
reportFinalName);
|
||
nombresOriginalesSubidos.Add(report.FileName);
|
||
nombresFinalesSubidos.Add($"{carpetaActualizacion}/{reportFinalName}");
|
||
}
|
||
|
||
var adjuntos = todos
|
||
.Where(t => !IsReportFileName(t.FileName))
|
||
.ToList();
|
||
|
||
if (adjuntos.Count > 0 && uploadMode == "merge")
|
||
{
|
||
Busy.Update(message: "Uniendo adjuntos nuevos en un unico PDF y subiendolo.", detail: "Paso 7 de 8");
|
||
var pdfBytes = PdfHelper.MergeFilesToPdf(adjuntos);
|
||
var pdfName = BuildMergedAttachmentsFileName(selectedDenuncias.Id_Denuncia, ahoraUtc);
|
||
var docUrl = await ApiDenuncias.UploadGestionaDocumentAsync(documentsTargetUrl, pdfBytes, pdfName);
|
||
if (string.IsNullOrWhiteSpace(documentoParaTramitar))
|
||
{
|
||
documentoParaTramitar = docUrl;
|
||
}
|
||
|
||
foreach (var adjunto in adjuntos)
|
||
{
|
||
nombresOriginalesSubidos.Add(adjunto.FileName);
|
||
}
|
||
|
||
nombresFinalesSubidos.Add($"{carpetaActualizacion}/{pdfName}");
|
||
}
|
||
else
|
||
{
|
||
int i = 1;
|
||
string baseNombre = string.IsNullOrWhiteSpace(nombreDocumentos) ? "" : $" {nombreDocumentos}";
|
||
|
||
foreach (var t in adjuntos)
|
||
{
|
||
Busy.Update(
|
||
message: $"Subiendo adjunto nuevo {i} de {adjuntos.Count}.",
|
||
detail: "Paso 7 de 8",
|
||
current: i,
|
||
total: adjuntos.Count);
|
||
|
||
var origName = t.FileName;
|
||
var content = t.Content;
|
||
var ext = Path.GetExtension(origName).ToLowerInvariant();
|
||
|
||
byte[] bytesParaSubir = content;
|
||
string finalName;
|
||
|
||
if (ext == ".txt")
|
||
{
|
||
bytesParaSubir = PdfHelper.MergeFilesToPdf(
|
||
new (string FileName, byte[] Content)[] { (origName, content) });
|
||
finalName = FixFileName($"Documento Adjunto {i}{baseNombre}.pdf");
|
||
}
|
||
else
|
||
{
|
||
finalName = FixFileName($"Documento Adjunto {i}{baseNombre}{ext}");
|
||
}
|
||
|
||
var docUrl = await ApiDenuncias.UploadGestionaDocumentAsync(documentsTargetUrl, bytesParaSubir, finalName);
|
||
if (string.IsNullOrWhiteSpace(documentoParaTramitar))
|
||
{
|
||
documentoParaTramitar = docUrl;
|
||
}
|
||
|
||
nombresOriginalesSubidos.Add(origName);
|
||
nombresFinalesSubidos.Add($"{carpetaActualizacion}/{finalName}");
|
||
i++;
|
||
}
|
||
}
|
||
|
||
selectedDenuncias.ArchivoElegido = string.Join(";", nombresFinalesSubidos);
|
||
|
||
if (!string.IsNullOrWhiteSpace(documentoParaTramitar))
|
||
{
|
||
Busy.Update(message: "Finalizando el documento principal en Gestiona.", detail: "Paso 8 de 8");
|
||
selectedGroup = NormalizeUpdateGroup(selectedGroup);
|
||
await ApiDenuncias.TramitarGestionaDocumentAsync(
|
||
documentoParaTramitar,
|
||
selectedGroup,
|
||
selectedDenuncias.Id_Denuncia,
|
||
isUpdate: true);
|
||
}
|
||
|
||
foreach (var orig in nombresOriginalesSubidos)
|
||
{
|
||
var f = existentesF.FirstOrDefault(x => x.NombreFichero == orig);
|
||
if (f != null)
|
||
{
|
||
f.Subido = true;
|
||
f.FechaSubida = ahoraUtc;
|
||
}
|
||
}
|
||
|
||
Busy.Update(message: "Actualizando trazabilidad en la base de datos.", detail: "Finalizando");
|
||
await DenunciaStore.MarkFicherosAsUploadedAsync(
|
||
selectedDenuncias.Id_Denuncia,
|
||
nombresOriginalesSubidos,
|
||
ahoraUtc);
|
||
|
||
selectedDenuncias.EsActualizacion = false;
|
||
selectedDenuncias.NombreDenuncia = nuevoAsunto;
|
||
selectedDenuncias.FechaSubidaAGestiona = ahoraUtc;
|
||
selectedDenuncias.UltimaSubidaGestionaTipo = "Actualización";
|
||
selectedDenuncias.UltimoGrupoAsignadoGestiona = GetGestionaGroupDisplay(selectedGroup);
|
||
await SincronizarExpedienteGestionaAsync(selectedDenuncias, fileUrl);
|
||
await ActualizarDenunciaAsync(selectedDenuncias);
|
||
var historialAviso = await RegistrarHistorialGestionaAsync(
|
||
selectedDenuncias,
|
||
"Actualización",
|
||
selectedGroup,
|
||
ahoraUtc,
|
||
string.Join("; ", nombresFinalesSubidos));
|
||
|
||
var denunciaProcesadaId = selectedDenuncias.Id_Denuncia;
|
||
actualizaciones.RemoveAll(x => x.Id_Denuncia == denunciaProcesadaId);
|
||
excludedUploadFiles.Remove(denunciaProcesadaId);
|
||
CloseModal();
|
||
|
||
var avisos = new List<string>();
|
||
if (ficherosVacios.Count > 0)
|
||
{
|
||
avisos.Add($"Se omitieron ficheros vacíos: {string.Join(", ", ficherosVacios)}.");
|
||
}
|
||
if (ficherosNoSeleccionados.Count > 0)
|
||
{
|
||
avisos.Add($"No se subieron por selección del usuario: {string.Join(", ", ficherosNoSeleccionados)}.");
|
||
}
|
||
if (thirdResult.Warnings.Count > 0)
|
||
{
|
||
avisos.AddRange(thirdResult.Warnings);
|
||
}
|
||
if (!string.IsNullOrWhiteSpace(historialAviso))
|
||
{
|
||
avisos.Add(historialAviso);
|
||
}
|
||
var expedienteInfo = string.IsNullOrWhiteSpace(selectedDenuncias.ExpedienteGestionaMostrable)
|
||
? string.Empty
|
||
: $" Nº expediente Gestiona: {selectedDenuncias.ExpedienteGestionaMostrable}.";
|
||
var resumen = expedienteCreadoEnGestiona
|
||
? $"Actualización #{denunciaProcesadaId}: se ha creado el expediente en Gestiona y se han subido los documentos.{expedienteInfo}"
|
||
: $"Actualización #{denunciaProcesadaId}: se han añadido los documentos al expediente de Gestiona.{expedienteInfo}";
|
||
operationNotice = avisos.Count > 0
|
||
? $"{resumen} {string.Join(" ", avisos)}"
|
||
: resumen;
|
||
StateHasChanged();
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
Console.Error.WriteLine($"Error al confirmar envío (Actualizaciones): {ex}");
|
||
operationError = $"No se ha podido completar la actualización #{selectedDenuncias?.Id_Denuncia}: {ex.Message}";
|
||
}
|
||
finally
|
||
{
|
||
isUploading = false;
|
||
StateHasChanged();
|
||
}
|
||
}
|
||
|
||
private bool IsFileSelectedForUpload(int denunciaId, string? fileName)
|
||
{
|
||
if (IsReportFileName(fileName))
|
||
{
|
||
return true;
|
||
}
|
||
|
||
return string.IsNullOrWhiteSpace(fileName) ||
|
||
!excludedUploadFiles.TryGetValue(denunciaId, out var excluded) ||
|
||
!excluded.Contains(fileName);
|
||
}
|
||
|
||
private void ToggleFileSelection(int denunciaId, string? fileName, bool selected)
|
||
{
|
||
if (string.IsNullOrWhiteSpace(fileName) || IsReportFileName(fileName))
|
||
{
|
||
return;
|
||
}
|
||
|
||
if (!excludedUploadFiles.TryGetValue(denunciaId, out var excluded))
|
||
{
|
||
excluded = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||
excludedUploadFiles[denunciaId] = excluded;
|
||
}
|
||
|
||
if (selected)
|
||
{
|
||
excluded.Remove(fileName);
|
||
if (excluded.Count == 0)
|
||
{
|
||
excludedUploadFiles.Remove(denunciaId);
|
||
}
|
||
}
|
||
else
|
||
{
|
||
excluded.Add(fileName);
|
||
}
|
||
}
|
||
|
||
private List<FicherosDenuncias> GetSelectedUploadFiles(int denunciaId, IEnumerable<FicherosDenuncias> files)
|
||
{
|
||
return files
|
||
.Where(file => IsReportFileName(file.NombreFichero) || IsFileSelectedForUpload(denunciaId, file.NombreFichero))
|
||
.ToList();
|
||
}
|
||
|
||
private List<string> GetExcludedUploadFileNames(int denunciaId, IEnumerable<FicherosDenuncias> files)
|
||
{
|
||
return files
|
||
.Where(file => !IsReportFileName(file.NombreFichero) && !IsFileSelectedForUpload(denunciaId, file.NombreFichero))
|
||
.Select(file => string.IsNullOrWhiteSpace(file.NombreFichero) ? "(sin nombre)" : file.NombreFichero)
|
||
.Distinct(StringComparer.OrdinalIgnoreCase)
|
||
.OrderBy(name => name, StringComparer.OrdinalIgnoreCase)
|
||
.ToList();
|
||
}
|
||
private static string BuildAttachmentContentUrl(int denunciaId, string? fileName)
|
||
{
|
||
return $"/api/denuncias/{denunciaId}/ficheros/content?fileName={Uri.EscapeDataString(fileName ?? string.Empty)}";
|
||
}
|
||
|
||
private static string FormatFileDate(DateTime date)
|
||
=> date == DateTime.MinValue
|
||
? "-"
|
||
: date.ToLocalTime().ToString("dd/MM/yyyy HH:mm", CultureInfo.InvariantCulture);
|
||
|
||
private string GetContentType(string fileName)
|
||
{
|
||
return Path.GetExtension(fileName).ToLowerInvariant() switch
|
||
{
|
||
".jpg" or ".jpeg" => "image/jpeg",
|
||
".png" => "image/png",
|
||
".gif" => "image/gif",
|
||
".pdf" => "application/pdf",
|
||
".txt" => "text/plain",
|
||
_ => "application/octet-stream",
|
||
};
|
||
}
|
||
|
||
private static string GetDocumentoLabel(DenunciasGestiona denuncia)
|
||
{
|
||
return string.IsNullOrWhiteSpace(denuncia.TipoDocumentoIdentificativo)
|
||
? "Documento identificativo"
|
||
: denuncia.TipoDocumentoIdentificativo;
|
||
}
|
||
|
||
private static string GetReadOnlyValue(string? value) =>
|
||
string.IsNullOrWhiteSpace(value) ? "—" : value;
|
||
|
||
private static bool IsExternalGestionaUpdate(DenunciasGestiona denuncia)
|
||
{
|
||
return denuncia.EsActualizacion &&
|
||
denuncia.EnGestiona &&
|
||
denuncia.FechaSubidaAGestiona == DateTime.MinValue;
|
||
}
|
||
|
||
private static bool HasPostalAddress(DenunciasGestiona denuncia)
|
||
{
|
||
return !string.IsNullOrWhiteSpace(denuncia.Direccion) ||
|
||
!string.IsNullOrWhiteSpace(denuncia.DireccionNumero) ||
|
||
!string.IsNullOrWhiteSpace(denuncia.Municipio) ||
|
||
!string.IsNullOrWhiteSpace(denuncia.Provincia) ||
|
||
!string.IsNullOrWhiteSpace(denuncia.CodigoPostal);
|
||
}
|
||
|
||
private static string BuildPostalAddressSummary(DenunciasGestiona denuncia)
|
||
{
|
||
var parts = new List<string>();
|
||
|
||
var street = string.Join(
|
||
' ',
|
||
new[]
|
||
{
|
||
denuncia.DireccionTipoVia,
|
||
denuncia.Direccion,
|
||
denuncia.DireccionNumero
|
||
}.Where(value => !string.IsNullOrWhiteSpace(value)));
|
||
if (!string.IsNullOrWhiteSpace(street))
|
||
{
|
||
parts.Add(street);
|
||
}
|
||
|
||
var extras = string.Join(
|
||
", ",
|
||
new[]
|
||
{
|
||
string.IsNullOrWhiteSpace(denuncia.DireccionBloque) ? null : $"Bloque {denuncia.DireccionBloque}",
|
||
string.IsNullOrWhiteSpace(denuncia.DireccionEscalera) ? null : $"Esc. {denuncia.DireccionEscalera}",
|
||
string.IsNullOrWhiteSpace(denuncia.DireccionPiso) ? null : $"Planta {denuncia.DireccionPiso}",
|
||
string.IsNullOrWhiteSpace(denuncia.DireccionPuerta) ? null : $"Puerta {denuncia.DireccionPuerta}",
|
||
denuncia.DireccionExtra
|
||
}.Where(value => !string.IsNullOrWhiteSpace(value)));
|
||
if (!string.IsNullOrWhiteSpace(extras))
|
||
{
|
||
parts.Add(extras);
|
||
}
|
||
|
||
var location = string.Join(
|
||
", ",
|
||
new[]
|
||
{
|
||
denuncia.CodigoPostal,
|
||
denuncia.Municipio,
|
||
denuncia.Provincia
|
||
}.Where(value => !string.IsNullOrWhiteSpace(value)));
|
||
if (!string.IsNullOrWhiteSpace(location))
|
||
{
|
||
parts.Add(location);
|
||
}
|
||
|
||
return string.Join(" | ", parts);
|
||
}
|
||
|
||
private List<FicherosDenuncias> GetPendingUpdateFilesForComplaint(DenunciasGestiona denuncia, List<FicherosDenuncias> files)
|
||
{
|
||
var cutoffDate = IsExternalGestionaUpdate(denuncia)
|
||
? externalUpdateCutoffDate
|
||
: null;
|
||
|
||
return GetPendingUpdateFiles(files, cutoffDate);
|
||
}
|
||
|
||
private static List<FicherosDenuncias> GetPendingUpdateFiles(List<FicherosDenuncias> files, DateOnly? externalUpdateCutoffDate = null)
|
||
{
|
||
var uploadedHashes = files
|
||
.Where(file => file.Subido && !string.IsNullOrWhiteSpace(file.ContentSha256))
|
||
.Select(file => file.ContentSha256)
|
||
.ToHashSet(StringComparer.OrdinalIgnoreCase);
|
||
|
||
var plannedHashes = new HashSet<string>(uploadedHashes, StringComparer.OrdinalIgnoreCase);
|
||
var pending = new List<FicherosDenuncias>();
|
||
|
||
var report = files
|
||
.Where(file => file.EsReport)
|
||
.OrderByDescending(file => file.Fecha)
|
||
.FirstOrDefault();
|
||
if (report is not null && !report.Subido)
|
||
{
|
||
if (string.IsNullOrWhiteSpace(report.ContentSha256) || plannedHashes.Add(report.ContentSha256))
|
||
{
|
||
pending.Add(report);
|
||
}
|
||
}
|
||
|
||
foreach (var file in files
|
||
.Where(file => !file.EsReport && !file.Subido)
|
||
.OrderBy(file => file.NombreFichero, StringComparer.OrdinalIgnoreCase))
|
||
{
|
||
if (externalUpdateCutoffDate.HasValue && !IsFileAfterCutoff(file, externalUpdateCutoffDate.Value))
|
||
{
|
||
continue;
|
||
}
|
||
|
||
if (string.IsNullOrWhiteSpace(file.ContentSha256))
|
||
{
|
||
pending.Add(file);
|
||
continue;
|
||
}
|
||
|
||
if (plannedHashes.Add(file.ContentSha256))
|
||
{
|
||
pending.Add(file);
|
||
}
|
||
}
|
||
|
||
return pending;
|
||
}
|
||
|
||
private static bool IsFileAfterCutoff(FicherosDenuncias file, DateOnly cutoffDate)
|
||
{
|
||
if (file.Fecha == DateTime.MinValue)
|
||
{
|
||
return true;
|
||
}
|
||
|
||
return DateOnly.FromDateTime(file.Fecha) > cutoffDate;
|
||
}
|
||
|
||
private static DateOnly? ParseConfiguredCutoffDate(string? value)
|
||
{
|
||
return DateOnly.TryParseExact(
|
||
value,
|
||
"yyyy-MM-dd",
|
||
CultureInfo.InvariantCulture,
|
||
DateTimeStyles.None,
|
||
out var parsed)
|
||
? parsed
|
||
: null;
|
||
}
|
||
|
||
private string GetExternalUpdateWarningText()
|
||
{
|
||
const string prefix = "Esta actualizacion corresponde a una denuncia que ya existia en Gestiona, pero no consta como subida desde esta app.";
|
||
return externalUpdateCutoffDate.HasValue
|
||
? $"{prefix} Como no tenemos hashes previos, solo se propondran como nuevos los adjuntos con fecha posterior a {externalUpdateCutoffDate.Value:dd/MM/yyyy}."
|
||
: $"{prefix} Configura una fecha de corte para que los adjuntos antiguos no aparezcan como nuevos.";
|
||
}
|
||
|
||
private string GetExternalUpdateFileReason()
|
||
{
|
||
return externalUpdateCutoffDate.HasValue
|
||
? $"Posterior al corte {externalUpdateCutoffDate.Value:dd/MM/yyyy}"
|
||
: "Sin historico local";
|
||
}
|
||
|
||
private static string NormalizeUpdateGroup(string? groupCode)
|
||
=> groupCode == "510" ? "510" : "600";
|
||
|
||
private static string GetGestionaGroupDisplay(string? groupCode)
|
||
{
|
||
return string.IsNullOrWhiteSpace(groupCode) ? string.Empty : groupCode.Trim();
|
||
}
|
||
|
||
private async Task<string?> RegistrarHistorialGestionaAsync(
|
||
DenunciasGestiona denuncia,
|
||
string tipoSubida,
|
||
string grupo,
|
||
DateTime uploadedAtUtc,
|
||
string documentos)
|
||
{
|
||
try
|
||
{
|
||
await ApiDenuncias.RegisterGestionaUploadHistoryAsync(
|
||
new GestionaUploadHistoryCreateRequest(
|
||
denuncia.Id_Denuncia,
|
||
denuncia.Expediente_Gestiona ?? string.Empty,
|
||
denuncia.ExpedienteGestionaMostrable,
|
||
tipoSubida,
|
||
grupo,
|
||
uploadedAtUtc,
|
||
denuncia.NombreDenuncia ?? string.Empty,
|
||
documentos));
|
||
return null;
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
Console.Error.WriteLine($"No se ha podido registrar el historico de subida a Gestiona: {ex}");
|
||
return "La subida se ha completado, pero no se ha podido registrar el histórico operativo en la aplicación.";
|
||
}
|
||
}
|
||
|
||
private async Task SincronizarExpedienteGestionaAsync(DenunciasGestiona denuncia, string fileUrl)
|
||
{
|
||
if (string.IsNullOrWhiteSpace(fileUrl))
|
||
{
|
||
return;
|
||
}
|
||
|
||
try
|
||
{
|
||
var expediente = await ApiDenuncias.GetGestionaExpedienteAsync(fileUrl);
|
||
if (expediente is null)
|
||
{
|
||
return;
|
||
}
|
||
|
||
denuncia.Expediente_Gestiona = expediente.FileUrl;
|
||
if (!string.IsNullOrWhiteSpace(expediente.CodigoExpediente))
|
||
{
|
||
denuncia.CodigoExpedienteGestiona = expediente.CodigoExpediente;
|
||
}
|
||
|
||
if (!string.IsNullOrWhiteSpace(expediente.FreeTitle))
|
||
{
|
||
denuncia.NombreDenuncia = expediente.FreeTitle;
|
||
}
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
Console.Error.WriteLine($"No se ha podido sincronizar el expediente de Gestiona: {ex}");
|
||
}
|
||
}
|
||
|
||
}
|
||
|