- Corrige la comparación del hash del report para detectar cambios y comentarios nuevos.

- Elimina la generación de documentos artificiales para comentarios.
- Añade los estados Nueva denuncia, Actualización ciudadano, Actividad OAAF y Actualizada.
- Incorpora circuitos específicos de comunicación SAJ y SDI.
- Registra correctamente el tipo de última subida en el histórico.
- Añade confirmación al asignar una denuncia a un grupo distinto al del usuario.
- Mejora etiquetas, mensajes de ayuda e instrucciones de la aplicación.
- Añade la persistencia técnica necesaria para identificar el origen de cada actualización.
This commit is contained in:
2026-07-29 14:47:35 +02:00
parent 6f9392f3d0
commit 8dedafdc07
23 changed files with 404 additions and 71 deletions

View File

@@ -13,6 +13,8 @@ namespace ApiDenuncias.Configuration
public string? CircuitUpdateTemplateName { get; set; } public string? CircuitUpdateTemplateName { get; set; }
public string? CircuitUpdateSajTemplateName { get; set; } public string? CircuitUpdateSajTemplateName { get; set; }
public string? CircuitUpdateSdiTemplateName { get; set; } public string? CircuitUpdateSdiTemplateName { get; set; }
public string? CircuitCommunicationSajTemplateName { get; set; }
public string? CircuitCommunicationSdiTemplateName { get; set; }
public string? CircuitSignerStampTitle { get; set; } public string? CircuitSignerStampTitle { get; set; }
public string? CircuitVersion { get; set; } public string? CircuitVersion { get; set; }
public string? DocumentMetadataLanguage { get; set; } public string? DocumentMetadataLanguage { get; set; }

View File

@@ -61,6 +61,16 @@ public sealed class ConfigurationController : ControllerBase
return Ok(await _workGroupService.GetAsync(cancellationToken)); return Ok(await _workGroupService.GetAsync(cancellationToken));
} }
[HttpGet("work-groups/current")]
public async Task<ActionResult<CurrentUserWorkGroupsDto>> GetCurrentUserWorkGroups(
CancellationToken cancellationToken)
{
var username = User.Identity?.Name ??
throw new InvalidOperationException("No hay usuario autenticado.");
return Ok(await _workGroupService.GetUserGroupsAsync(username, cancellationToken));
}
[HttpPut("work-groups/users/{username}")] [HttpPut("work-groups/users/{username}")]
[Authorize(Policy = "ConfigurationAdministrators")] [Authorize(Policy = "ConfigurationAdministrators")]
public async Task<ActionResult<WorkGroupAdministrationDto>> SetUserWorkGroups( public async Task<ActionResult<WorkGroupAdministrationDto>> SetUserWorkGroups(

View File

@@ -194,7 +194,8 @@ public sealed class GestionaController : ControllerBase
request.DocumentUrl, request.DocumentUrl,
request.AssignedGroupCode, request.AssignedGroupCode,
request.ComplaintId, request.ComplaintId,
request.IsUpdate); request.IsUpdate,
request.UpdateSource);
return Ok(new { ok = true }); return Ok(new { ok = true });
} }

View File

@@ -316,7 +316,12 @@ public sealed class InboxController : ControllerBase
json = null; json = null;
} }
var result = await _inboxService.ImportFromGlobalLeaksAsync(reportPackage, json, reportDetail, cancellationToken); var result = await _inboxService.ImportFromGlobalLeaksAsync(
reportPackage,
json,
reportDetail,
report,
cancellationToken);
if (result.ImportedCount > 0) if (result.ImportedCount > 0)
{ {
await _trackingService.MarkReportImportedAsync( await _trackingService.MarkReportImportedAsync(

View File

@@ -66,6 +66,7 @@ CREATE TABLE IF NOT EXISTS complaints (
gestiona_uploaded_at_utc DATETIME(6) NULL, gestiona_uploaded_at_utc DATETIME(6) NULL,
gestiona_last_upload_type TEXT NOT NULL, gestiona_last_upload_type TEXT NOT NULL,
gestiona_assigned_group TEXT NOT NULL, gestiona_assigned_group TEXT NOT NULL,
pending_update_source VARCHAR(256) NOT NULL DEFAULT '',
is_in_gestiona TINYINT(1) NOT NULL DEFAULT 0, is_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),

View File

@@ -84,6 +84,7 @@ public sealed class DenunciaInboxService
FileDownloadResult reportDownload, FileDownloadResult reportDownload,
FileDownloadResult? jsonDownload, FileDownloadResult? jsonDownload,
ReportDetailDto? reportDetail, ReportDetailDto? reportDetail,
ReportDto inboxReport,
CancellationToken cancellationToken = default) CancellationToken cancellationToken = default)
{ {
await EnsureStorageReadyAsync(cancellationToken); await EnsureStorageReadyAsync(cancellationToken);
@@ -102,7 +103,13 @@ public sealed class DenunciaInboxService
try try
{ {
var result = await ProcessGlobalLeaksPackageAsync(reportDownload.Content, sourceName, json, reportDetail, cancellationToken); var result = await ProcessGlobalLeaksPackageAsync(
reportDownload.Content,
sourceName,
json,
reportDetail,
inboxReport,
cancellationToken);
return new ImportSummary( return new ImportSummary(
1, 1,
result.ImportedCount, result.ImportedCount,
@@ -164,6 +171,7 @@ public sealed class DenunciaInboxService
string sourceName, string sourceName,
string? globalLeaksJson, string? globalLeaksJson,
ReportDetailDto? reportDetail, ReportDetailDto? reportDetail,
ReportDto inboxReport,
CancellationToken cancellationToken) CancellationToken cancellationToken)
{ {
using var packageStream = new MemoryStream(packageBytes, writable: false); using var packageStream = new MemoryStream(packageBytes, writable: false);
@@ -214,6 +222,12 @@ public sealed class DenunciaInboxService
$"No se ha podido determinar el identificador de la denuncia en {sourceName}."); $"No se ha podido determinar el identificador de la denuncia en {sourceName}.");
} }
denuncia.PendingUpdateSource = inboxReport.CitizenHasNewActivity
? ComplaintUpdateSources.Citizen
: inboxReport.ReceiverHasNewActivity
? ComplaintUpdateSources.Receiver
: string.Empty;
if (reportIsPdf) if (reportIsPdf)
{ {
reportText = BuildSyntheticReportText(denuncia); reportText = BuildSyntheticReportText(denuncia);
@@ -246,14 +260,17 @@ public sealed class DenunciaInboxService
var storedComplaint = await _denunciaStore.GetDenunciaByIdAsync(denuncia.Id_Denuncia, cancellationToken); var storedComplaint = await _denunciaStore.GetDenunciaByIdAsync(denuncia.Id_Denuncia, cancellationToken);
if (IsAlreadyUploadedToGestiona(storedComplaint)) if (IsAlreadyUploadedToGestiona(storedComplaint))
{ {
var storedFiles = await GetFicherosMetadataByDenunciaForImportAsync(denuncia.Id_Denuncia, cancellationToken); var storedFiles = await GetFicherosMetadataByDenunciaForImportAsync(
denuncia.Id_Denuncia,
cancellationToken);
if (!HasPendingFilesForGestiona(storedFiles)) if (!HasPendingFilesForGestiona(storedFiles))
{ {
storedComplaint!.EsActualizacion = false; storedComplaint!.EsActualizacion = false;
storedComplaint.EnGestiona = true; storedComplaint.EnGestiona = true;
storedComplaint.PendingUpdateSource = string.Empty;
await _denunciaStore.UpsertDenunciaAsync(storedComplaint, cancellationToken); await _denunciaStore.UpsertDenunciaAsync(storedComplaint, cancellationToken);
warnings.Add($"La denuncia #{denuncia.Id_Denuncia} ya esta en Gestiona y no tiene documentos nuevos pendientes de subir."); warnings.Add(BuildNoCitizenUpdateWarning(inboxReport));
return new ProcessPackageResult(denuncia.Id_Denuncia, 0, warnings); return new ProcessPackageResult(denuncia.Id_Denuncia, 0, warnings);
} }
} }
@@ -379,6 +396,13 @@ public sealed class DenunciaInboxService
} }
} }
private static string BuildNoCitizenUpdateWarning(ReportDto inboxReport)
{
return inboxReport.ReceiverHasNewActivity && !inboxReport.CitizenHasNewActivity
? "La actividad detectada procede de la OAAF, pero el report descargado no contiene cambios pendientes de subir a Gestiona."
: "La denuncia ya esta en Gestiona y el report no contiene documentos ni comentarios nuevos del ciudadano pendientes de subir.";
}
private static bool HasPendingFilesForGestiona(IReadOnlyList<FicherosDenuncias> files) private static bool HasPendingFilesForGestiona(IReadOnlyList<FicherosDenuncias> files)
{ {
var plannedHashes = files var plannedHashes = files
@@ -591,6 +615,7 @@ public sealed class DenunciaInboxService
target.Pais = source.Pais; target.Pais = source.Pais;
target.CamposFormularioJson = source.CamposFormularioJson; target.CamposFormularioJson = source.CamposFormularioJson;
target.TextoOriginalReport = source.TextoOriginalReport; target.TextoOriginalReport = source.TextoOriginalReport;
target.PendingUpdateSource = source.PendingUpdateSource;
} }
private static bool IsSupportedAttachmentEntry(ZipArchiveEntry entry) private static bool IsSupportedAttachmentEntry(ZipArchiveEntry entry)

View File

@@ -280,6 +280,7 @@ public sealed class EncryptedDenunciaStore : IDenunciaStore, IFilteredDenunciaSt
FechaSubidaAGestiona = source.FechaSubidaAGestiona, FechaSubidaAGestiona = source.FechaSubidaAGestiona,
UltimaSubidaGestionaTipo = source.UltimaSubidaGestionaTipo, UltimaSubidaGestionaTipo = source.UltimaSubidaGestionaTipo,
UltimoGrupoAsignadoGestiona = source.UltimoGrupoAsignadoGestiona, UltimoGrupoAsignadoGestiona = source.UltimoGrupoAsignadoGestiona,
PendingUpdateSource = source.PendingUpdateSource,
EnGestiona = source.EnGestiona, EnGestiona = source.EnGestiona,
EnRechazada = source.EnRechazada, EnRechazada = source.EnRechazada,
@@ -361,6 +362,7 @@ public sealed class EncryptedDenunciaStore : IDenunciaStore, IFilteredDenunciaSt
target.FechaSubidaAGestiona = stored.FechaSubidaAGestiona; target.FechaSubidaAGestiona = stored.FechaSubidaAGestiona;
target.UltimaSubidaGestionaTipo = stored.UltimaSubidaGestionaTipo; target.UltimaSubidaGestionaTipo = stored.UltimaSubidaGestionaTipo;
target.UltimoGrupoAsignadoGestiona = stored.UltimoGrupoAsignadoGestiona; target.UltimoGrupoAsignadoGestiona = stored.UltimoGrupoAsignadoGestiona;
target.PendingUpdateSource = stored.PendingUpdateSource;
target.EnGestiona = stored.EnGestiona; target.EnGestiona = stored.EnGestiona;
target.EnRechazada = stored.EnRechazada; target.EnRechazada = stored.EnRechazada;
} }

View File

@@ -3,6 +3,7 @@ using System.Security.Cryptography;
using System.Globalization; using System.Globalization;
using System.Text; using System.Text;
using System.Text.Json; using System.Text.Json;
using GestionaDenuncias.Shared.Models;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
namespace ApiDenuncias.Services; namespace ApiDenuncias.Services;
@@ -124,11 +125,20 @@ public sealed class GestionaDocumentWorkflowService
string documentUrl, string documentUrl,
string assignedGroupCode, string assignedGroupCode,
int? complaintId = null, int? complaintId = null,
bool isUpdate = false) bool isUpdate = false,
string? updateSource = null)
{ {
var docUrlAbs = EnsureAbsoluteGestionaUrl(documentUrl, GestionaApiBase); var docUrlAbs = EnsureAbsoluteGestionaUrl(documentUrl, GestionaApiBase);
var operationLabel = isUpdate ? $"actualizacion grupo {NormalizeGroupCode(assignedGroupCode)}" : "nueva denuncia"; var operationLabel = ComplaintUpdateSources.IsReceiver(updateSource)
var (templateHref, payload) = await ResolveCircuitTemplatePayloadAsync(docUrlAbs, isUpdate, assignedGroupCode); ? $"comunicacion OAAF grupo {NormalizeGroupCode(assignedGroupCode)}"
: isUpdate
? $"actualizacion grupo {NormalizeGroupCode(assignedGroupCode)}"
: "nueva denuncia";
var (templateHref, payload) = await ResolveCircuitTemplatePayloadAsync(
docUrlAbs,
isUpdate,
assignedGroupCode,
updateSource);
var (success, statusCode, body) = await TryPostCircuitAsync(docUrlAbs, payload); var (success, statusCode, body) = await TryPostCircuitAsync(docUrlAbs, payload);
if (success) if (success)
{ {
@@ -155,7 +165,8 @@ public sealed class GestionaDocumentWorkflowService
private async Task<(string TemplateHref, string Payload)> ResolveCircuitTemplatePayloadAsync( private async Task<(string TemplateHref, string Payload)> ResolveCircuitTemplatePayloadAsync(
string documentUrl, string documentUrl,
bool isUpdate, bool isUpdate,
string? assignedGroupCode) string? assignedGroupCode,
string? updateSource)
{ {
var templatesUrl = $"{documentUrl.TrimEnd('/')}/circuit/templates"; var templatesUrl = $"{documentUrl.TrimEnd('/')}/circuit/templates";
var templates = await GetCircuitTemplatesAsync(templatesUrl); var templates = await GetCircuitTemplatesAsync(templatesUrl);
@@ -164,7 +175,7 @@ public sealed class GestionaDocumentWorkflowService
throw new InvalidOperationException("Gestiona no ha devuelto plantillas de circuito para el documento."); throw new InvalidOperationException("Gestiona no ha devuelto plantillas de circuito para el documento.");
} }
var selection = GetOperationCircuitTemplateSelection(isUpdate, assignedGroupCode); var selection = GetOperationCircuitTemplateSelection(isUpdate, assignedGroupCode, updateSource);
if (!string.IsNullOrWhiteSpace(selection.TemplateName)) if (!string.IsNullOrWhiteSpace(selection.TemplateName))
{ {
var resolved = await TryResolveCircuitTemplatePayloadByNameAsync(templates, selection.TemplateName); var resolved = await TryResolveCircuitTemplatePayloadByNameAsync(templates, selection.TemplateName);
@@ -211,7 +222,10 @@ public sealed class GestionaDocumentWorkflowService
"No se puede seleccionar de forma inequivoca la plantilla de circuito. Configura Gestiona:CircuitTemplateName o Gestiona:CircuitSignerStampTitle."); "No se puede seleccionar de forma inequivoca la plantilla de circuito. Configura Gestiona:CircuitTemplateName o Gestiona:CircuitSignerStampTitle.");
} }
private CircuitTemplateSelection GetOperationCircuitTemplateSelection(bool isUpdate, string? assignedGroupCode) private CircuitTemplateSelection GetOperationCircuitTemplateSelection(
bool isUpdate,
string? assignedGroupCode,
string? updateSource)
{ {
if (!isUpdate) if (!isUpdate)
{ {
@@ -221,6 +235,25 @@ public sealed class GestionaDocumentWorkflowService
OperationLabel: "nueva denuncia"); OperationLabel: "nueva denuncia");
} }
if (ComplaintUpdateSources.IsReceiver(updateSource))
{
return NormalizeGroupCode(assignedGroupCode) switch
{
"510" => new CircuitTemplateSelection(
_configuration["Gestiona:CircuitCommunicationSdiTemplateName"],
Required: true,
OperationLabel: "comunicacion SDI a denunciante"),
"600" => new CircuitTemplateSelection(
_configuration["Gestiona:CircuitCommunicationSajTemplateName"],
Required: true,
OperationLabel: "comunicacion SAJ a denunciante"),
_ => throw new InvalidOperationException(
"Las comunicaciones de la OAAF solo pueden tramitarse con los grupos 510 o 600.")
};
}
var defaultUpdateTemplateName = FirstConfigured( var defaultUpdateTemplateName = FirstConfigured(
_configuration["Gestiona:CircuitTemplateName"], _configuration["Gestiona:CircuitTemplateName"],
_configuration["Gestiona:CircuitUpdateTemplateName"]); _configuration["Gestiona:CircuitUpdateTemplateName"]);

View File

@@ -950,6 +950,15 @@ public sealed class GlobalLeaksClient
private static DateTimeOffset? GetActivityReference(ReportDto report, DateTimeOffset? fallbackReference) private static DateTimeOffset? GetActivityReference(ReportDto report, DateTimeOffset? fallbackReference)
{ {
if (report.AlreadyInGestiona)
{
var lastGestionaUpload = ParseDate(report.LastGestionaUploadAt);
if (lastGestionaUpload is not null)
{
return lastGestionaUpload;
}
}
return ParseDate(report.LastDownloadedAt) ?? return ParseDate(report.LastDownloadedAt) ??
fallbackReference ?? fallbackReference ??
ParseDate(report.LastAccess) ?? ParseDate(report.LastAccess) ??

View File

@@ -102,6 +102,7 @@ public sealed class InboxTrackingService : IInboxTrackingService
DownloadedByAnotherUser = meta?.DownloadedByAnotherUser ?? false, DownloadedByAnotherUser = meta?.DownloadedByAnotherUser ?? false,
LastDownloadedByUsername = meta?.LastDownloadedByUsername, LastDownloadedByUsername = meta?.LastDownloadedByUsername,
LastDownloadedAt = meta?.LastDownloadedAtUtc?.ToString("O", CultureInfo.InvariantCulture), LastDownloadedAt = meta?.LastDownloadedAtUtc?.ToString("O", CultureInfo.InvariantCulture),
LastGestionaUploadAt = meta?.LastGestionaUploadAtUtc?.ToString("O", CultureInfo.InvariantCulture),
AlreadyImported = meta?.AlreadyImported ?? false, AlreadyImported = meta?.AlreadyImported ?? false,
AlreadyInGestiona = meta?.AlreadyInGestiona ?? false, AlreadyInGestiona = meta?.AlreadyInGestiona ?? false,
OwnerUsername = meta?.OwnerUsername, OwnerUsername = meta?.OwnerUsername,
@@ -535,6 +536,15 @@ public sealed class InboxTrackingService : IInboxTrackingService
owner.username AS owner_username, owner.username AS owner_username,
ir.imported_to_store_at_utc, ir.imported_to_store_at_utc,
COALESCE(c.is_in_gestiona, 0) AS already_in_gestiona, COALESCE(c.is_in_gestiona, 0) AS already_in_gestiona,
COALESCE(
(
SELECT MAX(history.uploaded_at_utc)
FROM gestiona_upload_history history
WHERE history.external_report_id =
COALESCE(ir.imported_complaint_report_id, ir.progressive_id)
),
c.gestiona_uploaded_at_utc
) AS last_gestiona_upload_at_utc,
CASE WHEN uir.last_downloaded_at_utc IS NULL THEN 0 ELSE 1 END AS downloaded_by_current_user, CASE WHEN uir.last_downloaded_at_utc IS NULL THEN 0 ELSE 1 END AS downloaded_by_current_user,
CASE WHEN ir.owner_user_id = @userId THEN 1 ELSE 0 END AS owned_by_current_user, CASE WHEN ir.owner_user_id = @userId THEN 1 ELSE 0 END AS owned_by_current_user,
CASE CASE
@@ -599,6 +609,7 @@ public sealed class InboxTrackingService : IInboxTrackingService
DownloadedByAnotherUser = downloadedByAnotherUser, DownloadedByAnotherUser = downloadedByAnotherUser,
LastDownloadedByUsername = lastDownloadedByUsername, LastDownloadedByUsername = lastDownloadedByUsername,
LastDownloadedAtUtc = GetDateTimeOffset(reader, "last_downloaded_at_utc"), LastDownloadedAtUtc = GetDateTimeOffset(reader, "last_downloaded_at_utc"),
LastGestionaUploadAtUtc = GetDateTimeOffset(reader, "last_gestiona_upload_at_utc"),
AlreadyImported = !reader.IsDBNull(reader.GetOrdinal("imported_to_store_at_utc")), AlreadyImported = !reader.IsDBNull(reader.GetOrdinal("imported_to_store_at_utc")),
AlreadyInGestiona = reader.GetInt32(reader.GetOrdinal("already_in_gestiona")) == 1, AlreadyInGestiona = reader.GetInt32(reader.GetOrdinal("already_in_gestiona")) == 1,
OwnerUsername = ownerUsername, OwnerUsername = ownerUsername,
@@ -748,6 +759,7 @@ public sealed class InboxTrackingService : IInboxTrackingService
public bool DownloadedByAnotherUser { get; init; } public bool DownloadedByAnotherUser { get; init; }
public string? LastDownloadedByUsername { get; init; } public string? LastDownloadedByUsername { get; init; }
public DateTimeOffset? LastDownloadedAtUtc { get; init; } public DateTimeOffset? LastDownloadedAtUtc { get; init; }
public DateTimeOffset? LastGestionaUploadAtUtc { get; init; }
public bool AlreadyImported { get; init; } public bool AlreadyImported { get; init; }
public bool AlreadyInGestiona { get; init; } public bool AlreadyInGestiona { get; init; }
public string? OwnerUsername { get; init; } public string? OwnerUsername { get; init; }

View File

@@ -116,6 +116,7 @@ public sealed class MySqlDenunciaStore : IDenunciaStore
gestiona_uploaded_at_utc, gestiona_uploaded_at_utc,
gestiona_last_upload_type, gestiona_last_upload_type,
gestiona_assigned_group, gestiona_assigned_group,
pending_update_source,
is_in_gestiona, is_in_gestiona,
is_rejected, is_rejected,
key_date, key_date,
@@ -184,6 +185,7 @@ public sealed class MySqlDenunciaStore : IDenunciaStore
("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_last_upload_type", "`gestiona_last_upload_type` VARCHAR(64) NOT NULL DEFAULT ''"),
("complaints", "gestiona_assigned_group", "`gestiona_assigned_group` VARCHAR(255) NOT NULL DEFAULT ''"), ("complaints", "gestiona_assigned_group", "`gestiona_assigned_group` VARCHAR(255) NOT NULL DEFAULT ''"),
("complaints", "pending_update_source", "`pending_update_source` VARCHAR(256) NOT NULL DEFAULT ''"),
("inbox_reports", "owner_user_id", "`owner_user_id` BIGINT NULL"), ("inbox_reports", "owner_user_id", "`owner_user_id` BIGINT NULL"),
("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"),
@@ -711,6 +713,7 @@ public sealed class MySqlDenunciaStore : IDenunciaStore
gestiona_uploaded_at_utc, gestiona_uploaded_at_utc,
gestiona_last_upload_type, gestiona_last_upload_type,
gestiona_assigned_group, gestiona_assigned_group,
pending_update_source,
is_in_gestiona, is_in_gestiona,
is_rejected, is_rejected,
key_date, key_date,
@@ -783,6 +786,7 @@ public sealed class MySqlDenunciaStore : IDenunciaStore
@gestionaUploadedAtUtc, @gestionaUploadedAtUtc,
@gestionaLastUploadType, @gestionaLastUploadType,
@gestionaAssignedGroup, @gestionaAssignedGroup,
@pendingUpdateSource,
@isInGestiona, @isInGestiona,
@isRejected, @isRejected,
@keyDate, @keyDate,
@@ -855,6 +859,7 @@ public sealed class MySqlDenunciaStore : IDenunciaStore
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_last_upload_type = VALUES(gestiona_last_upload_type),
gestiona_assigned_group = VALUES(gestiona_assigned_group), gestiona_assigned_group = VALUES(gestiona_assigned_group),
pending_update_source = VALUES(pending_update_source),
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),
@@ -932,6 +937,7 @@ public sealed class MySqlDenunciaStore : IDenunciaStore
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("@gestionaLastUploadType", denuncia.UltimaSubidaGestionaTipo ?? string.Empty);
command.Parameters.AddWithValue("@gestionaAssignedGroup", denuncia.UltimoGrupoAsignadoGestiona ?? string.Empty); command.Parameters.AddWithValue("@gestionaAssignedGroup", denuncia.UltimoGrupoAsignadoGestiona ?? string.Empty);
command.Parameters.AddWithValue("@pendingUpdateSource", denuncia.PendingUpdateSource ?? string.Empty);
command.Parameters.AddWithValue("@isInGestiona", denuncia.EnGestiona); command.Parameters.AddWithValue("@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));
@@ -996,9 +1002,6 @@ public sealed class MySqlDenunciaStore : IDenunciaStore
description = @description, description = @description,
attachment_date_utc = @attachmentDateUtc, attachment_date_utc = @attachmentDateUtc,
notes = @notes, notes = @notes,
content = @content,
content_mime_type = @contentMimeType,
content_sha256 = @contentSha256,
uploaded_to_gestiona = CASE uploaded_to_gestiona = CASE
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
@@ -1007,6 +1010,9 @@ public sealed class MySqlDenunciaStore : IDenunciaStore
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,
content = @content,
content_mime_type = @contentMimeType,
content_sha256 = @contentSha256,
key_date = @keyDate, key_date = @keyDate,
encryption_scheme = @encryptionScheme, encryption_scheme = @encryptionScheme,
encrypted_at_utc = @encryptedAtUtc, encrypted_at_utc = @encryptedAtUtc,
@@ -1721,6 +1727,7 @@ public sealed class MySqlDenunciaStore : IDenunciaStore
FechaSubidaAGestiona = GetDateTime(record, "gestiona_uploaded_at_utc"), FechaSubidaAGestiona = GetDateTime(record, "gestiona_uploaded_at_utc"),
UltimaSubidaGestionaTipo = GetString(record, "gestiona_last_upload_type"), UltimaSubidaGestionaTipo = GetString(record, "gestiona_last_upload_type"),
UltimoGrupoAsignadoGestiona = GetString(record, "gestiona_assigned_group"), UltimoGrupoAsignadoGestiona = GetString(record, "gestiona_assigned_group"),
PendingUpdateSource = GetString(record, "pending_update_source"),
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"),

View File

@@ -69,6 +69,42 @@ public sealed class WorkGroupAdministrationService
.ToArray()); .ToArray());
} }
public async Task<CurrentUserWorkGroupsDto> GetUserGroupsAsync(
string username,
CancellationToken cancellationToken = default)
{
await _denunciaStore.EnsureSchemaAsync(cancellationToken);
var normalizedUsername = username?.Trim();
if (string.IsNullOrWhiteSpace(normalizedUsername))
{
throw new InvalidOperationException("No hay usuario autenticado.");
}
await using var connection = await OpenConnectionAsync(cancellationToken);
const string sql = """
SELECT wg.code
FROM app_users au
INNER JOIN app_user_groups aug ON aug.app_user_id = au.id
INNER JOIN work_groups wg
ON wg.id = aug.work_group_id
AND wg.is_active = 1
WHERE LOWER(au.username) = LOWER(@username)
ORDER BY wg.code;
""";
var groupCodes = new List<string>();
await using var command = new MySqlCommand(sql, connection);
command.Parameters.AddWithValue("@username", normalizedUsername);
await using var reader = await command.ExecuteReaderAsync(cancellationToken);
while (await reader.ReadAsync(cancellationToken))
{
groupCodes.Add(reader.GetString(reader.GetOrdinal("code")));
}
return new CurrentUserWorkGroupsDto(normalizedUsername, groupCodes);
}
public async Task<WorkGroupAdministrationDto> UpdateUserGroupsAsync( public async Task<WorkGroupAdministrationDto> UpdateUserGroupsAsync(
string username, string username,
IEnumerable<string> groupCodes, IEnumerable<string> groupCodes,

View File

@@ -44,6 +44,8 @@
"CircuitNewComplaintTemplateName": "CT-Nueva denuncia", "CircuitNewComplaintTemplateName": "CT-Nueva denuncia",
"CircuitUpdateSajTemplateName": "CT-Actualización denuncia SAJ", "CircuitUpdateSajTemplateName": "CT-Actualización denuncia SAJ",
"CircuitUpdateSdiTemplateName": "CT-Actualización denuncia SDI", "CircuitUpdateSdiTemplateName": "CT-Actualización denuncia SDI",
"CircuitCommunicationSajTemplateName": "CT-Comunicación SAJ a denunciante",
"CircuitCommunicationSdiTemplateName": "CT-Comunicación SDI a denunciante",
"CircuitSignerStampTitle": "oaaf-complaints-tramit", "CircuitSignerStampTitle": "oaaf-complaints-tramit",
"CircuitVersion": "2", "CircuitVersion": "2",
"DocumentMetadataLanguage": "es", "DocumentMetadataLanguage": "es",

View File

@@ -95,7 +95,8 @@ public sealed record GestionaTramitarDocumentoRequest(
string DocumentUrl, string DocumentUrl,
string AssignedGroupCode, string AssignedGroupCode,
int? ComplaintId, int? ComplaintId,
bool IsUpdate = false); bool IsUpdate = false,
string? UpdateSource = null);
public sealed record ManualPurgeRequest(string Date); public sealed record ManualPurgeRequest(string Date);
@@ -125,6 +126,10 @@ public sealed record WorkGroupAdministrationDto(
IReadOnlyList<WorkGroupDto> Groups, IReadOnlyList<WorkGroupDto> Groups,
IReadOnlyList<UserWorkGroupDto> Users); IReadOnlyList<UserWorkGroupDto> Users);
public sealed record CurrentUserWorkGroupsDto(
string Username,
IReadOnlyList<string> GroupCodes);
public sealed record UpdateUserWorkGroupsRequest( public sealed record UpdateUserWorkGroupsRequest(
IReadOnlyList<string> GroupCodes); IReadOnlyList<string> GroupCodes);

View File

@@ -0,0 +1,13 @@
namespace GestionaDenuncias.Shared.Models;
public static class ComplaintUpdateSources
{
public const string Citizen = "citizen";
public const string Receiver = "receiver";
public static bool IsCitizen(string? value)
=> string.Equals(value, Citizen, StringComparison.OrdinalIgnoreCase);
public static bool IsReceiver(string? value)
=> string.Equals(value, Receiver, StringComparison.OrdinalIgnoreCase);
}

View File

@@ -79,6 +79,7 @@ public class DenunciasGestiona
public DateTime FechaSubidaAGestiona { get; set; } = DateTime.MinValue; public DateTime FechaSubidaAGestiona { get; set; } = DateTime.MinValue;
public string UltimaSubidaGestionaTipo { get; set; } = string.Empty; public string UltimaSubidaGestionaTipo { get; set; } = string.Empty;
public string UltimoGrupoAsignadoGestiona { get; set; } = string.Empty; public string UltimoGrupoAsignadoGestiona { get; set; } = string.Empty;
public string PendingUpdateSource { get; set; } = string.Empty;
public bool EnGestiona { get; set; } public bool EnGestiona { get; set; }
public bool EnRechazada { get; set; } public bool EnRechazada { get; set; }

View File

@@ -30,6 +30,7 @@ public sealed record ReportDto
public bool DownloadedByAnotherUser { get; init; } public bool DownloadedByAnotherUser { get; init; }
public string? LastDownloadedByUsername { get; init; } public string? LastDownloadedByUsername { get; init; }
public string? LastDownloadedAt { get; init; } public string? LastDownloadedAt { get; init; }
public string? LastGestionaUploadAt { get; init; }
public bool AlreadyImported { get; init; } public bool AlreadyImported { get; init; }
public bool AlreadyInGestiona { get; init; } public bool AlreadyInGestiona { get; init; }
public string? OwnerUsername { get; init; } public string? OwnerUsername { get; init; }

View File

@@ -734,7 +734,7 @@ else
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); selectedGroup = NormalizeUpdateGroup(d.UltimoGrupoAsignadoGestiona);
operationError = string.Empty; operationError = string.Empty;
operationNotice = string.Empty; operationNotice = string.Empty;
@@ -844,12 +844,19 @@ else
return; return;
} }
if (!await ConfirmSelectedGroupAsync())
{
return;
}
try try
{ {
isUploading = true; isUploading = true;
operationError = string.Empty; operationError = string.Empty;
operationNotice = string.Empty; operationNotice = string.Empty;
selectedGroup = NormalizeUpdateGroup(selectedGroup); selectedGroup = NormalizeUpdateGroup(selectedGroup);
var pendingUpdateSource = selectedDenuncias.PendingUpdateSource;
var uploadType = GetUpdateUploadType(pendingUpdateSource, 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.");
@@ -1035,7 +1042,8 @@ else
documentoParaTramitar, documentoParaTramitar,
selectedGroup, selectedGroup,
selectedDenuncias.Id_Denuncia, selectedDenuncias.Id_Denuncia,
isUpdate: true); isUpdate: true,
updateSource: pendingUpdateSource);
} }
foreach (var orig in nombresOriginalesSubidos) foreach (var orig in nombresOriginalesSubidos)
@@ -1057,13 +1065,14 @@ else
selectedDenuncias.EsActualizacion = false; selectedDenuncias.EsActualizacion = false;
selectedDenuncias.NombreDenuncia = nuevoAsunto; selectedDenuncias.NombreDenuncia = nuevoAsunto;
selectedDenuncias.FechaSubidaAGestiona = ahoraUtc; selectedDenuncias.FechaSubidaAGestiona = ahoraUtc;
selectedDenuncias.UltimaSubidaGestionaTipo = "Actualización"; selectedDenuncias.UltimaSubidaGestionaTipo = uploadType;
selectedDenuncias.UltimoGrupoAsignadoGestiona = GetGestionaGroupDisplay(selectedGroup); selectedDenuncias.UltimoGrupoAsignadoGestiona = GetGestionaGroupDisplay(selectedGroup);
selectedDenuncias.PendingUpdateSource = string.Empty;
await SincronizarExpedienteGestionaAsync(selectedDenuncias, fileUrl); await SincronizarExpedienteGestionaAsync(selectedDenuncias, fileUrl);
await ActualizarDenunciaAsync(selectedDenuncias); await ActualizarDenunciaAsync(selectedDenuncias);
var historialAviso = await RegistrarHistorialGestionaAsync( var historialAviso = await RegistrarHistorialGestionaAsync(
selectedDenuncias, selectedDenuncias,
"Actualización", uploadType,
selectedGroup, selectedGroup,
ahoraUtc, ahoraUtc,
string.Join("; ", nombresFinalesSubidos)); string.Join("; ", nombresFinalesSubidos));
@@ -1358,13 +1367,52 @@ else
} }
private static string NormalizeUpdateGroup(string? groupCode) private static string NormalizeUpdateGroup(string? groupCode)
=> groupCode == "510" ? "510" : "600"; => groupCode?.Trim().StartsWith("510", StringComparison.Ordinal) == true
? "510"
: "600";
private static string GetGestionaGroupDisplay(string? groupCode) private static string GetGestionaGroupDisplay(string? groupCode)
{ {
return string.IsNullOrWhiteSpace(groupCode) ? string.Empty : groupCode.Trim(); return string.IsNullOrWhiteSpace(groupCode) ? string.Empty : groupCode.Trim();
} }
private static string GetUpdateUploadType(string? updateSource, string groupCode)
{
if (!ComplaintUpdateSources.IsReceiver(updateSource))
{
return "Actualización";
}
return NormalizeUpdateGroup(groupCode) == "510"
? "Comunic. SDI a denunciante"
: "Comunic. SAJ a denunciante";
}
private async Task<bool> ConfirmSelectedGroupAsync()
{
try
{
var currentGroups = await ApiDenuncias.GetCurrentUserWorkGroupsAsync();
if (currentGroups.GroupCodes.Contains(selectedGroup, StringComparer.OrdinalIgnoreCase))
{
return true;
}
var configuredGroups = currentGroups.GroupCodes.Count == 0
? "ningún grupo"
: string.Join(", ", currentGroups.GroupCodes);
return await JSRuntime.InvokeAsync<bool>(
"confirm",
$"El grupo de destino {selectedGroup} no pertenece a tus grupos configurados ({configuredGroups}). " +
"Si continúas, la denuncia cambiará su asignación en Gestiona a un grupo distinto de los tuyos. ¿Deseas continuar?");
}
catch (Exception ex)
{
operationError = $"No se han podido comprobar tus grupos antes de la subida: {ex.Message}";
return false;
}
}
private async Task<string?> RegistrarHistorialGestionaAsync( private async Task<string?> RegistrarHistorialGestionaAsync(
DenunciasGestiona denuncia, DenunciasGestiona denuncia,
string tipoSubida, string tipoSubida,

View File

@@ -314,12 +314,12 @@
<th>#</th> <th>#</th>
<th>Canal</th> <th>Canal</th>
<th>Presentacion</th> <th>Presentacion</th>
<th>Actividad ciudadano</th> <th title="Fecha de la última aportación o comentario realizado por el ciudadano.">Actividad ciudadano</th>
<th>Actividad OAAF</th> <th title="Fecha de la última comunicación o fichero incorporado por un gestor de la OAAF.">Actividad OAAF</th>
<th>Estado</th> <th title="Resume si la denuncia es nueva, tiene cambios pendientes o ya está actualizada en Gestiona.">Estado</th>
<th>Acceso</th> <th title="Indica si tu usuario gestor del buzón puede acceder a esta denuncia.">Acceso</th>
<th>Seguimiento</th> <th title="Indica si el cambio está pendiente, descargado en la aplicación o ya incorporado a Gestiona.">Gestiona</th>
<th class="inbox-action-cell">Detalle</th> <th class="inbox-action-cell" title="Abre el detalle disponible en GlobalLeaks.">Detalle</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
@@ -361,17 +361,17 @@
<td class="inbox-activity-cell">@FormatCitizenActivity(report)</td> <td class="inbox-activity-cell">@FormatCitizenActivity(report)</td>
<td class="inbox-activity-cell" title="@GetReceiverActivityTitle(report)">@FormatReceiverActivity(report)</td> <td class="inbox-activity-cell" title="@GetReceiverActivityTitle(report)">@FormatReceiverActivity(report)</td>
<td> <td>
<span class="badge @GetStatusBadgeCss(report)"> <span class="badge @GetStatusBadgeCss(report)" title="@GetStatusHelp(report)">
@GetStatusLabel(report) @GetStatusLabel(report)
</span> </span>
</td> </td>
<td> <td>
<span class="badge @GetAccessBadgeCss(report)"> <span class="badge @GetAccessBadgeCss(report)" title="@GetAccessHelp(report)">
@GetAccessLabel(report) @GetAccessLabel(report)
</span> </span>
</td> </td>
<td class="inbox-tracking-cell" title="@(report.TrackingNote ?? string.Empty)"> <td class="inbox-tracking-cell">
<span class="badge @GetTrackingBadgeCss(report)">@GetTrackingLabel(report)</span> <span class="badge @GetTrackingBadgeCss(report)" title="@GetTrackingHelp(report)">@GetTrackingLabel(report)</span>
</td> </td>
<td class="inbox-action-cell"> <td class="inbox-action-cell">
<button type="button" <button type="button"
@@ -1282,7 +1282,7 @@
if (report.CitizenHasNewActivity) if (report.CitizenHasNewActivity)
{ {
return "Actualizacion ciudadano"; return "Actualización ciudadano";
} }
if (IsReceiverOnlyUpdate(report)) if (IsReceiverOnlyUpdate(report))
@@ -1290,14 +1290,19 @@
return "Actividad OAAF"; return "Actividad OAAF";
} }
if (report.Updated) if (report.AlreadyInGestiona && !report.ActivityAnalyzed)
{
return "Sin comprobar";
}
if (IsUpToDateInGestiona(report))
{ {
return "Actualizada"; return "Actualizada";
} }
return string.Equals(report.Status, "closed", StringComparison.OrdinalIgnoreCase) return string.Equals(report.Status, "closed", StringComparison.OrdinalIgnoreCase)
? "Cerrada" ? "Cerrada"
: "Abierta"; : "Nueva denuncia";
} }
private static string GetStatusBadgeCss(ReportDto report) private static string GetStatusBadgeCss(ReportDto report)
@@ -1317,7 +1322,12 @@
return "bg-secondary"; return "bg-secondary";
} }
if (report.Updated) if (report.AlreadyInGestiona && !report.ActivityAnalyzed)
{
return "bg-warning text-dark";
}
if (IsUpToDateInGestiona(report))
{ {
return "bg-light text-dark"; return "bg-light text-dark";
} }
@@ -1327,6 +1337,47 @@
: "bg-primary"; : "bg-primary";
} }
private static bool IsUpToDateInGestiona(ReportDto report)
{
if (!report.AlreadyInGestiona)
{
return false;
}
var lastUpload = ParseDate(report.LastGestionaUploadAt);
var latestActivity = new[]
{
ParseDate(report.CitizenLastActivity),
ParseDate(report.ReceiverLastActivity)
}
.Where(value => value is not null)
.Select(value => value!.Value)
.DefaultIfEmpty()
.Max();
if (lastUpload is not null && latestActivity != default)
{
return lastUpload.Value >= latestActivity;
}
return report.ActivityAnalyzed &&
!report.CitizenHasNewActivity &&
!report.ReceiverHasNewActivity;
}
private static string GetStatusHelp(ReportDto report)
=> GetStatusLabel(report) switch
{
"Nueva denuncia" => "La denuncia todavía no tiene expediente creado desde la aplicación y está pendiente de actuación.",
"Actualización ciudadano" => "El ciudadano ha añadido un comentario, un fichero o una modificación posterior a la última subida a Gestiona.",
"Actividad OAAF" => "Un gestor de la OAAF ha realizado una comunicación o añadido un fichero posterior a la última subida a Gestiona.",
"Actualizada" => "La última subida a Gestiona es igual o posterior a la actividad del ciudadano y de la OAAF detectada en el buzón.",
"Sin comprobar" => "No se ha podido comparar en este momento la actividad del buzón con la última subida a Gestiona.",
"Cerrada" => "La denuncia figura cerrada en GlobalLeaks.",
"Sin leer" => "La denuncia todavía no se ha abierto con este usuario en GlobalLeaks.",
_ => string.Empty
};
private static string GetAccessLabel(ReportDto report) private static string GetAccessLabel(ReportDto report)
=> report.Accessible switch => report.Accessible switch
{ {
@@ -1343,6 +1394,14 @@
_ => "bg-light text-dark" _ => "bg-light text-dark"
}; };
private static string GetAccessHelp(ReportDto report)
=> report.Accessible switch
{
true => "Tu usuario gestor del buzón tiene acceso a esta denuncia.",
false => "GlobalLeaks indica que tu usuario gestor del buzón no tiene acceso a esta denuncia.",
_ => "GlobalLeaks no ha informado si tu usuario puede acceder a esta denuncia."
};
private static bool CanUseReport(ReportDto report) private static bool CanUseReport(ReportDto report)
=> report.Accessible != false; => report.Accessible != false;
@@ -1422,6 +1481,35 @@
return "bg-light text-dark"; return "bg-light text-dark";
} }
private static string GetTrackingHelp(ReportDto report)
{
if (report.AlreadyInGestiona)
{
return "La denuncia ya tiene un expediente creado en Gestiona. El estado indica si existe actividad posterior pendiente.";
}
if (report.DownloadedByAnotherUser)
{
return string.IsNullOrWhiteSpace(report.TrackingNote)
? "Otro usuario ya la descargó en la aplicación, pero todavía no se ha materializado la subida a Gestiona."
: $"{report.TrackingNote}. Todavía no se ha materializado la subida a Gestiona.";
}
if (report.DownloadedByCurrentUser)
{
return string.IsNullOrWhiteSpace(report.TrackingNote)
? "Ya la descargaste en la aplicación, pero todavía no se ha materializado la subida a Gestiona."
: $"{report.TrackingNote}. Todavía no se ha materializado la subida a Gestiona.";
}
if (report.AlreadyImported)
{
return "La denuncia está incorporada a la aplicación, pero todavía no se ha subido a Gestiona.";
}
return "La denuncia todavía no se ha descargado ni subido a Gestiona.";
}
private static string? GetReportRowCss(ReportDto report) private static string? GetReportRowCss(ReportDto report)
{ {
if (report.Accessible == false) if (report.Accessible == false)

View File

@@ -114,28 +114,28 @@ else
<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>Asunto:</strong> @GetHeaderSubject(denuncia, latestHistory)</span> <span title="Asunto con el que se identifica el expediente en Gestiona."><strong>Asunto:</strong> @GetHeaderSubject(denuncia, latestHistory)</span>
@if (!string.IsNullOrWhiteSpace(denuncia.ExpedienteGestionaMostrable)) @if (!string.IsNullOrWhiteSpace(denuncia.ExpedienteGestionaMostrable))
{ {
<span><strong>Nº expediente:</strong> @denuncia.ExpedienteGestionaMostrable</span> <span title="Número asignado al expediente por Gestiona."><strong>Nº expediente:</strong> @denuncia.ExpedienteGestionaMostrable</span>
} }
@if (!string.IsNullOrWhiteSpace(GetUploadType(denuncia, latestHistory))) @if (!string.IsNullOrWhiteSpace(GetUploadType(denuncia, latestHistory)))
{ {
<span><strong>Última subida:</strong> @GetUploadType(denuncia, latestHistory)</span> <span title="Tipo de la última operación enviada desde la aplicación a Gestiona."><strong>Última subida:</strong> @GetUploadType(denuncia, latestHistory)</span>
} }
@if (!string.IsNullOrWhiteSpace(GetAssignedGroup(denuncia, latestHistory))) @if (!string.IsNullOrWhiteSpace(GetAssignedGroup(denuncia, latestHistory)))
{ {
<span><strong>Grupo asignado:</strong> @FormatGroupCode(GetAssignedGroup(denuncia, latestHistory))</span> <span title="Grupo al que quedó asignado el expediente tras la última subida."><strong>Asignación en Gestiona:</strong> @FormatGroupCode(GetAssignedGroup(denuncia, latestHistory))</span>
} }
@if (!string.IsNullOrWhiteSpace(GetUploadUser(latestHistory))) @if (!string.IsNullOrWhiteSpace(GetUploadUser(latestHistory)))
{ {
<span><strong>Usuario subida:</strong> @GetUploadUser(latestHistory)</span> <span title="Usuario de la aplicación que realizó la última subida."><strong>Usuario subida:</strong> @GetUploadUser(latestHistory)</span>
} }
<span><strong>Fecha de Subida:</strong> @FormatUploadDate(uploadMoment)</span> <span title="Fecha de la última operación enviada a Gestiona."><strong>Fecha de Subida:</strong> @FormatUploadDate(uploadMoment)</span>
<span><strong>Hora de Subida:</strong> @FormatUploadTime(uploadMoment)</span> <span title="Hora local de la última operación enviada a Gestiona."><strong>Hora de Subida:</strong> @FormatUploadTime(uploadMoment)</span>
@if (!string.IsNullOrWhiteSpace(GetAuditDateText(denuncia))) @if (!string.IsNullOrWhiteSpace(GetAuditDateText(denuncia)))
{ {
<span><strong>Última actividad Gestiona:</strong> @GetAuditDateText(denuncia)</span> <span title="Último movimiento registrado en la auditoría del expediente de Gestiona."><strong>Última actividad Gestiona:</strong> @GetAuditDateText(denuncia)</span>
} }
</div> </div>
</div> </div>
@@ -331,16 +331,16 @@ else
<div class="card-header"> <div class="card-header">
<h5 class="mb-0">Denuncia ID: @item.DenunciaId</h5> <h5 class="mb-0">Denuncia ID: @item.DenunciaId</h5>
<div class="header-info"> <div class="header-info">
<span><strong>Asunto:</strong> @DisplayOrDash(item.Asunto)</span> <span title="Asunto con el que se identifica el expediente en Gestiona."><strong>Asunto:</strong> @DisplayOrDash(item.Asunto)</span>
<span><strong>Nº expediente:</strong> @DisplayOrDash(item.CodigoExpedienteGestiona)</span> <span title="Número asignado al expediente por Gestiona."><strong>Nº expediente:</strong> @DisplayOrDash(item.CodigoExpedienteGestiona)</span>
<span><strong>Última subida:</strong> @DisplayOrDash(item.TipoSubida)</span> <span title="Tipo de la última operación enviada desde la aplicación a Gestiona."><strong>Última subida:</strong> @DisplayOrDash(item.TipoSubida)</span>
<span><strong>Grupo asignado:</strong> @DisplayOrDash(FormatGroupCode(item.GrupoAsignado))</span> <span title="Grupo al que quedó asignado el expediente tras la última subida."><strong>Asignación en Gestiona:</strong> @DisplayOrDash(FormatGroupCode(item.GrupoAsignado))</span>
@if (!string.IsNullOrWhiteSpace(GetUploadUser(item))) @if (!string.IsNullOrWhiteSpace(GetUploadUser(item)))
{ {
<span><strong>Usuario subida:</strong> @GetUploadUser(item)</span> <span title="Usuario de la aplicación que realizó la última subida."><strong>Usuario subida:</strong> @GetUploadUser(item)</span>
} }
<span><strong>Fecha de Subida:</strong> @FormatUploadDate(uploadMoment)</span> <span title="Fecha de la última operación enviada a Gestiona."><strong>Fecha de Subida:</strong> @FormatUploadDate(uploadMoment)</span>
<span><strong>Hora de Subida:</strong> @FormatUploadTime(uploadMoment)</span> <span title="Hora local de la última operación enviada a Gestiona."><strong>Hora de Subida:</strong> @FormatUploadTime(uploadMoment)</span>
<span class="text-muted">Detalle no disponible por purga criptográfica.</span> <span class="text-muted">Detalle no disponible por purga criptográfica.</span>
</div> </div>
</div> </div>

View File

@@ -56,7 +56,7 @@
<li>Abre la denuncia para revisar sus datos y adjuntos.</li> <li>Abre la denuncia para revisar sus datos y adjuntos.</li>
<li>En la lista de ficheros, deja marcado solo lo que quieras subir.</li> <li>En la lista de ficheros, deja marcado solo lo que quieras subir.</li>
<li>El report de la denuncia se sube siempre y no se puede desmarcar.</li> <li>El report de la denuncia se sube siempre y no se puede desmarcar.</li>
<li>Pulsa <strong>Configurar subida</strong> para indicar asunto, grupo destino y modo de subida.</li> <li>Pulsa <strong>Configurar apertura expediente</strong> para indicar asunto, grupo destino y modo de subida.</li>
<li>Confirma para crear el expediente, vincular el tercero y subir los documentos a Gestiona.</li> <li>Confirma para crear el expediente, vincular el tercero y subir los documentos a Gestiona.</li>
<li>Si la denuncia no procede, usa <strong>Rechazar denuncia</strong> e indica el motivo.</li> <li>Si la denuncia no procede, usa <strong>Rechazar denuncia</strong> e indica el motivo.</li>
</ul> </ul>
@@ -87,14 +87,15 @@
<div class="card-body"> <div class="card-body">
<h2 class="h5">Actualizaciones</h2> <h2 class="h5">Actualizaciones</h2>
<p> <p>
Esta pantalla recoge comunicaciones o adjuntos nuevos sobre denuncias que ya tienen expediente en Gestiona. Esta pantalla recoge comentarios, comunicaciones o adjuntos nuevos sobre denuncias que ya tienen expediente en Gestiona.
</p> </p>
<ul class="mb-0"> <ul class="mb-0">
<li>Revisa la actualizacion y sus ficheros.</li> <li>Revisa la actualizacion y sus ficheros.</li>
<li>La app propone los adjuntos que parecen nuevos.</li> <li>La app propone los adjuntos que parecen nuevos.</li>
<li>Puedes desmarcar los adjuntos que no quieras subir.</li> <li>Puedes desmarcar los adjuntos que no quieras subir.</li>
<li>El report de la actualizacion se mantiene obligatorio.</li> <li>El report de la actualizacion se mantiene obligatorio.</li>
<li>Confirma la subida para a<EFBFBD>adir los nuevos documentos al expediente existente.</li> <li>Pulsa <strong>Configurar actualización expediente</strong> y confirma para añadir el cambio al expediente existente.</li>
<li>Si la actividad procede de la OAAF, la aplicación utiliza el aviso correspondiente al grupo SAJ o SDI.</li>
</ul> </ul>
</div> </div>
</div> </div>
@@ -105,12 +106,12 @@
<div class="card-body"> <div class="card-body">
<h2 class="h5">Gestiona</h2> <h2 class="h5">Gestiona</h2>
<p> <p>
Aqui se consultan las denuncias que ya se han enviado a Gestiona. Aquí se almacena el histórico permanente de los movimientos enviados desde esta aplicación a Gestiona.
</p> </p>
<ul class="mb-0"> <ul class="mb-0">
<li>Comprueba el numero de expediente y la fecha de envio.</li> <li>Comprueba el número de expediente, el tipo de subida, el usuario que la realizó y la asignación en Gestiona.</li>
<li>Accede al enlace del expediente cuando necesites revisar la tramitacion en Gestiona.</li> <li>Despliega una operación del día para consultar sus datos y documentos mientras estén disponibles.</li>
<li>Usa esta pantalla como seguimiento de lo que ya salio de Pendientes o Actualizaciones.</li> <li>Por requisitos de seguridad ENS, los detalles sensibles de días anteriores dejan de estar disponibles; la cabecera operativa y la trazabilidad permanecen visibles.</li>
</ul> </ul>
</div> </div>
</div> </div>
@@ -121,7 +122,7 @@
<div class="card-body"> <div class="card-body">
<h2 class="h5">Rechazados</h2> <h2 class="h5">Rechazados</h2>
<p> <p>
Aqui quedan las denuncias que se han descartado desde Pendientes. Aquí quedan las denuncias o actualizaciones que no se han subido a Gestiona porque se descartaron desde la pantalla de trabajo.
</p> </p>
<ul class="mb-0"> <ul class="mb-0">
<li>Consulta el motivo indicado al rechazar.</li> <li>Consulta el motivo indicado al rechazar.</li>
@@ -165,4 +166,4 @@
<div class="alert alert-warning mt-3 mb-0"> <div class="alert alert-warning mt-3 mb-0">
Si una pantalla indica que una denuncia no esta disponible, no intentes tramitarla desde otra ruta: revisa la bandeja de Entrada o consulta con soporte del sistema. Si una pantalla indica que una denuncia no esta disponible, no intentes tramitarla desde otra ruta: revisa la bandeja de Entrada o consulta con soporte del sistema.
</div> </div>
</div> </div>

View File

@@ -602,7 +602,7 @@ else
600. Asuntos Jurídicos y Protecció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" <input class="form-check-input"
type="radio" type="radio"
name="selectedGroup" name="selectedGroup"
@@ -613,17 +613,6 @@ else
510. SDI Investigación Entradas 510. SDI Investigación Entradas
</label> </label>
</div> </div>
<div class="form-check">
<input class="form-check-input"
type="radio"
name="selectedGroup"
id="grupo700"
checked='@(selectedGroup == "700")'
@onclick='() => selectedGroup = "700"' />
<label class="form-check-label" for="grupo700">
700. RESPONSABLE DEL SERVICIO
</label>
</div> *@
<!-- DATOS DEL TERCERO --> <!-- DATOS DEL TERCERO -->
@{ @{
@@ -963,6 +952,11 @@ else
return; return;
} }
if (!await ConfirmSelectedGroupAsync())
{
return;
}
try try
{ {
isUploading = true; isUploading = true;
@@ -1348,6 +1342,31 @@ else
$"Puedes {action}la porque pertenece a un usuario de tu grupo. Deseas continuar?"); $"Puedes {action}la porque pertenece a un usuario de tu grupo. Deseas continuar?");
} }
private async Task<bool> ConfirmSelectedGroupAsync()
{
try
{
var currentGroups = await ApiDenuncias.GetCurrentUserWorkGroupsAsync();
if (currentGroups.GroupCodes.Contains(selectedGroup, StringComparer.OrdinalIgnoreCase))
{
return true;
}
var configuredGroups = currentGroups.GroupCodes.Count == 0
? "ningún grupo"
: string.Join(", ", currentGroups.GroupCodes);
return await JSRuntime.InvokeAsync<bool>(
"confirm",
$"El grupo de destino {selectedGroup} no pertenece a tus grupos configurados ({configuredGroups}). " +
"Si continúas, la denuncia quedará asignada en Gestiona a un grupo distinto de los tuyos. ¿Deseas continuar?");
}
catch (Exception ex)
{
operationError = $"No se han podido comprobar tus grupos antes de la subida: {ex.Message}";
return false;
}
}
private void CloseModal() private void CloseModal()
{ {
showModal = false; showModal = false;

View File

@@ -203,10 +203,16 @@ public sealed class ApiDenunciasClient
string assignedGroupCode, string assignedGroupCode,
int? complaintId, int? complaintId,
bool isUpdate = false, bool isUpdate = false,
string? updateSource = null,
CancellationToken cancellationToken = default) CancellationToken cancellationToken = default)
=> PostAsync( => PostAsync(
"api/gestiona/documents/tramitar", "api/gestiona/documents/tramitar",
new GestionaTramitarDocumentoRequest(documentUrl, assignedGroupCode, complaintId, isUpdate), new GestionaTramitarDocumentoRequest(
documentUrl,
assignedGroupCode,
complaintId,
isUpdate,
updateSource),
cancellationToken); cancellationToken);
public Task<List<ExpedienteTerceroDto>> GetGestionaExpedientesPorTerceroAsync( public Task<List<ExpedienteTerceroDto>> GetGestionaExpedientesPorTerceroAsync(
@@ -273,6 +279,12 @@ public sealed class ApiDenunciasClient
"api/configuration/work-groups", "api/configuration/work-groups",
cancellationToken); cancellationToken);
public Task<CurrentUserWorkGroupsDto> GetCurrentUserWorkGroupsAsync(
CancellationToken cancellationToken = default)
=> GetAsync<CurrentUserWorkGroupsDto>(
"api/configuration/work-groups/current",
cancellationToken);
public Task<WorkGroupAdministrationDto> UpdateUserWorkGroupsAsync( public Task<WorkGroupAdministrationDto> UpdateUserWorkGroupsAsync(
string username, string username,
IReadOnlyList<string> groupCodes, IReadOnlyList<string> groupCodes,