- Adaptados los avisos al importar, tramitar, rechazar y cambiar la asignación de denuncias de otros usuarios o grupos. - Añadidos textos y botones específicos para cada operación. - Actualizadas las instrucciones, sustituyendo “modal” por “ventana de configuración de la subida”. - Aclarado el funcionamiento del asunto, grupo de destino y tercero.
1540 lines
57 KiB
Plaintext
1540 lines
57 KiB
Plaintext
@page "/Entrada"
|
|
@rendermode @(new InteractiveServerRenderMode(prerender: false))
|
|
@attribute [Authorize]
|
|
@implements IAsyncDisposable
|
|
@using System.Globalization
|
|
@using System.Text.RegularExpressions
|
|
@using GestionaDenunciasAN.Models
|
|
@inject AuthenticationStateProvider AuthenticationStateProvider
|
|
@inject ApiDenunciasClient ApiDenuncias
|
|
@inject IJSRuntime JSRuntime
|
|
@inject UiBusyService Busy
|
|
@inject UiDialogService Dialogs
|
|
|
|
<PageTitle>Entrada de denuncias</PageTitle>
|
|
|
|
<style>
|
|
.report-detail-overlay {
|
|
position: fixed;
|
|
inset: 0;
|
|
z-index: 4500;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
padding: clamp(1rem, 3vw, 2rem);
|
|
background: rgba(6, 22, 41, 0.62);
|
|
overflow: hidden;
|
|
}
|
|
|
|
.report-detail-dialog {
|
|
width: min(820px, 100%);
|
|
max-height: min(88vh, 860px);
|
|
display: flex;
|
|
}
|
|
|
|
.report-detail-card {
|
|
width: 100%;
|
|
display: flex;
|
|
flex-direction: column;
|
|
overflow: hidden;
|
|
border: 1px solid rgba(215, 228, 241, 0.92);
|
|
border-radius: 22px;
|
|
background: #ffffff;
|
|
box-shadow: 0 28px 90px rgba(6, 22, 41, 0.38);
|
|
color: var(--app-ink, #12395f);
|
|
}
|
|
|
|
.report-detail-header,
|
|
.report-detail-footer {
|
|
flex: 0 0 auto;
|
|
background: #ffffff;
|
|
}
|
|
|
|
.report-detail-header {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
gap: 1rem;
|
|
padding: 1.25rem 1.35rem 1rem;
|
|
border-bottom: 1px solid var(--app-border, #d7e4f1);
|
|
}
|
|
|
|
.report-detail-body {
|
|
flex: 1 1 auto;
|
|
overflow-y: auto;
|
|
padding: 1rem 1.35rem;
|
|
background: #ffffff;
|
|
}
|
|
|
|
.report-detail-footer {
|
|
display: flex;
|
|
justify-content: flex-end;
|
|
padding: 1rem 1.35rem;
|
|
border-top: 1px solid var(--app-border, #d7e4f1);
|
|
}
|
|
|
|
.report-detail-close {
|
|
flex: 0 0 auto;
|
|
}
|
|
|
|
.report-row-disabled {
|
|
opacity: 0.62;
|
|
}
|
|
|
|
.report-row-disabled td {
|
|
cursor: not-allowed;
|
|
}
|
|
|
|
.inbox-table-wrap {
|
|
overflow-x: auto;
|
|
-webkit-overflow-scrolling: touch;
|
|
}
|
|
|
|
.inbox-table {
|
|
min-width: 1080px;
|
|
font-size: 0.84rem;
|
|
white-space: nowrap;
|
|
}
|
|
|
|
.inbox-table th,
|
|
.inbox-table td {
|
|
vertical-align: middle;
|
|
padding: 0.45rem 0.5rem;
|
|
}
|
|
|
|
.inbox-report-cell {
|
|
width: 4.5rem;
|
|
}
|
|
|
|
.inbox-channel-cell {
|
|
width: 10.5rem;
|
|
max-width: 10.5rem;
|
|
}
|
|
|
|
.inbox-channel-cell .text-truncate {
|
|
max-width: 100%;
|
|
}
|
|
|
|
.inbox-tracking-cell {
|
|
width: 11rem;
|
|
max-width: 11rem;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
}
|
|
|
|
.inbox-activity-cell {
|
|
width: 9.25rem;
|
|
max-width: 12rem;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
}
|
|
|
|
.inbox-action-cell {
|
|
width: 5.5rem;
|
|
text-align: right;
|
|
}
|
|
|
|
|
|
.inbox-detail-button {
|
|
padding-inline: 0.55rem;
|
|
white-space: nowrap;
|
|
}
|
|
|
|
@@media (max-width: 768px) {
|
|
.inbox-table {
|
|
min-width: 1020px;
|
|
font-size: 0.8rem;
|
|
}
|
|
|
|
.inbox-table th,
|
|
.inbox-table td {
|
|
padding: 0.4rem 0.42rem;
|
|
}
|
|
}
|
|
</style>
|
|
|
|
<div class="container-fluid py-4 px-3 px-xl-4">
|
|
<div class="d-flex flex-column flex-lg-row justify-content-between align-items-lg-center gap-3 mb-4">
|
|
<div>
|
|
<h3 class="mb-1">Entrada de denuncias</h3>
|
|
<p class="text-muted mb-0">
|
|
La app mantiene su propia sesion activa. Cuando caduque GlobalLeaks, aqui solo hara falta renovar el 2FA
|
|
para seguir trayendo denuncias.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
@if (!string.IsNullOrWhiteSpace(StatusMessage))
|
|
{
|
|
<div class="alert @StatusCss mb-4">@StatusMessage</div>
|
|
}
|
|
|
|
@if (!string.IsNullOrWhiteSpace(FilterWarningMessage))
|
|
{
|
|
<div class="alert @FilterWarningCss mb-4">@FilterWarningMessage</div>
|
|
}
|
|
|
|
<div class="card shadow-sm mb-3">
|
|
<div class="card-body py-3">
|
|
<div class="row g-3 align-items-end">
|
|
<div class="col-xl-5 col-lg-6">
|
|
<h5 class="card-title mb-2">Sesion GlobalLeaks</h5>
|
|
<div class="d-flex flex-wrap gap-3 small">
|
|
<span><strong>Usuario:</strong> @CurrentUsername</span>
|
|
<span><strong>Estado:</strong> @SessionStatusText</span>
|
|
<span>
|
|
<strong>Ultima descarga:</strong>
|
|
@(UserInboxState.LastDownloadedReportMomentUtc is null
|
|
? "Sin descargas previas"
|
|
: UserInboxState.LastDownloadedReportMomentUtc.Value.ToLocalTime().ToString("dd/MM/yyyy HH:mm"))
|
|
</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="col-xl-5 col-lg-4">
|
|
<label class="form-label mb-1">Nuevo codigo 2FA</label>
|
|
<input class="form-control"
|
|
@bind="RenewAuthcode"
|
|
maxlength="6"
|
|
inputmode="numeric"
|
|
disabled="@(!RenewPrepared || RenewBusy)"
|
|
placeholder="123456" />
|
|
</div>
|
|
|
|
<div class="col-xl-2 col-lg-2">
|
|
<button type="button" class="btn btn-primary w-100" @onclick="RenewSessionAsync" disabled="@RenewBusy">
|
|
@(RenewBusy ? SessionRenewBusyText : SessionRenewButtonText)
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="card shadow-sm">
|
|
<div class="card-body">
|
|
<div class="d-flex flex-column flex-md-row justify-content-between align-items-md-center gap-3 mb-3">
|
|
<div>
|
|
<h5 class="card-title mb-1">Bandeja GlobalLeaks</h5>
|
|
<p class="text-muted mb-0">
|
|
Puedes revisar solo lo nuevo, las actualizaciones desde tu ultima descarga o un intervalo de fechas.
|
|
</p>
|
|
</div>
|
|
<button type="button" class="btn btn-outline-primary" @onclick="LoadReportsAsync" disabled="@ReportsBusy || !CanUseGlobalLeaks">
|
|
@(ReportsBusy ? "Actualizando..." : "Actualizar denuncias")
|
|
</button>
|
|
</div>
|
|
|
|
<div class="row g-3 mb-3">
|
|
<div class="col-md-4">
|
|
<label class="form-label">Tipo</label>
|
|
<select class="form-select" value="@Filter" @onchange="OnFilterChanged">
|
|
<option value="all">Todas</option>
|
|
<option value="new">Nuevas / sin leer</option>
|
|
<option value="updated">Actualizaciones del ciudadano</option>
|
|
<option value="receiver">Actividad OAAF</option>
|
|
</select>
|
|
</div>
|
|
|
|
<div class="col-md-4">
|
|
<label class="form-label">Periodo</label>
|
|
<select class="form-select" value="@DateScope" @onchange="OnDateScopeChanged">
|
|
<option value="all">Todo el buzon</option>
|
|
<option value="since_mine">Desde mi ultima descarga</option>
|
|
<option value="range">Intervalo de fechas</option>
|
|
</select>
|
|
</div>
|
|
|
|
<div class="col-md-4">
|
|
<label class="form-label">Canal</label>
|
|
<select class="form-select" value="@SelectedChannel" @onchange="OnChannelChanged">
|
|
<option value="">Todos</option>
|
|
@foreach (var context in Contexts)
|
|
{
|
|
<option value="@context.Id">@context.Name</option>
|
|
}
|
|
</select>
|
|
</div>
|
|
|
|
<div class="col-md-3">
|
|
<label class="form-label">Desde</label>
|
|
<input class="form-control"
|
|
type="date"
|
|
value="@DateFrom"
|
|
disabled="@(DateScope != "range")"
|
|
@onchange="OnDateFromChanged" />
|
|
</div>
|
|
|
|
<div class="col-md-3">
|
|
<label class="form-label">Hasta</label>
|
|
<input class="form-control"
|
|
type="date"
|
|
value="@DateTo"
|
|
disabled="@(DateScope != "range")"
|
|
@onchange="OnDateToChanged" />
|
|
</div>
|
|
|
|
<div class="col-md-6">
|
|
<label class="form-label">Buscar</label>
|
|
<input class="form-control"
|
|
value="@SearchTerm"
|
|
@oninput="OnSearchChanged"
|
|
placeholder="# denuncia o canal" />
|
|
</div>
|
|
</div>
|
|
|
|
<div class="d-flex flex-column flex-md-row justify-content-between align-items-md-center gap-3 mb-3">
|
|
<div class="text-muted">
|
|
@VisibleReports.Count denuncia(s) visibles, @SelectedReportsCount seleccionada(s).
|
|
@if (VisibleReports.Any(report => report.Accessible == false))
|
|
{
|
|
<span class="d-block small text-danger">
|
|
@VisibleReports.Count(report => report.Accessible == false) denuncia(s) marcadas por GlobalLeaks como no accesibles para este usuario.
|
|
</span>
|
|
}
|
|
@if (UserInboxState.LastDownloadedReportMomentUtc is not null)
|
|
{
|
|
<span class="d-block small">
|
|
Referencia de ultima descarga: @UserInboxState.LastDownloadedReportMomentUtc.Value.ToLocalTime().ToString("dd/MM/yyyy HH:mm")
|
|
</span>
|
|
}
|
|
</div>
|
|
<div class="d-flex gap-2">
|
|
<button type="button" class="btn btn-outline-secondary btn-sm" @onclick="ToggleSelectAll" disabled="@(!VisibleReports.Any(CanUseReport))">
|
|
@SelectAllLabel
|
|
</button>
|
|
<button type="button" class="btn btn-success btn-sm" @onclick="ImportSelectedAsync" disabled="@(!CanImportSelected)">
|
|
@(ImportBusy ? "Importando..." : "Importar seleccionadas")
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="table-responsive inbox-table-wrap">
|
|
<table class="table table-hover table-sm align-middle inbox-table">
|
|
<thead>
|
|
<tr>
|
|
<th style="width: 3rem;"></th>
|
|
<th>#</th>
|
|
<th>Canal</th>
|
|
<th>Presentacion</th>
|
|
<th title="Fecha de la última aportación o comentario realizado por el ciudadano.">Actividad ciudadano</th>
|
|
<th title="Fecha de la última comunicación o fichero incorporado por un gestor de la OAAF.">Actividad OAAF</th>
|
|
<th title="Resume si la denuncia es nueva, tiene cambios pendientes o ya está actualizada en Gestiona.">Estado</th>
|
|
<th title="Indica si tu usuario gestor del buzón puede acceder a esta denuncia.">Acceso</th>
|
|
<th title="Indica si el cambio está pendiente, descargado en la aplicación o ya incorporado a Gestiona.">Gestiona</th>
|
|
<th class="inbox-action-cell" title="Abre el detalle disponible en GlobalLeaks.">Detalle</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
@if (!CanUseGlobalLeaks)
|
|
{
|
|
<tr>
|
|
<td colspan="10" class="text-muted">
|
|
Renueva la sesion de GlobalLeaks con un 2FA valido para cargar la bandeja.
|
|
</td>
|
|
</tr>
|
|
}
|
|
else if (ReportsBusy)
|
|
{
|
|
<tr>
|
|
<td colspan="10" class="text-muted">Cargando denuncias...</td>
|
|
</tr>
|
|
}
|
|
else if (!VisibleReports.Any())
|
|
{
|
|
<tr>
|
|
<td colspan="10" class="text-muted">No hay denuncias con los filtros actuales.</td>
|
|
</tr>
|
|
}
|
|
else
|
|
{
|
|
@foreach (var report in VisibleReports)
|
|
{
|
|
<tr @key="report.Id" class="@GetReportRowCss(report)">
|
|
<td>
|
|
<input type="checkbox"
|
|
checked="@SelectedIds.Contains(report.Id)"
|
|
disabled="@(!CanUseReport(report) || ImportBusy)"
|
|
title="@GetReportActionBlockReason(report)"
|
|
@onchange="@((ChangeEventArgs args) => ToggleSelection(report, args))" />
|
|
</td>
|
|
<td class="inbox-report-cell"><strong>#@(report.Progressive ?? 0)</strong></td>
|
|
<td class="inbox-channel-cell" title="@(report.ContextName ?? report.ContextId ?? string.Empty)"><span class="d-inline-block text-truncate">@(report.ContextName ?? report.ContextId ?? "-")</span></td>
|
|
<td>@FormatDate(report.CreationDate)</td>
|
|
<td class="inbox-activity-cell">@FormatCitizenActivity(report)</td>
|
|
<td class="inbox-activity-cell" title="@GetReceiverActivityTitle(report)">@FormatReceiverActivity(report)</td>
|
|
<td>
|
|
<span class="badge @GetStatusBadgeCss(report)" title="@GetStatusHelp(report)">
|
|
@GetStatusLabel(report)
|
|
</span>
|
|
</td>
|
|
<td>
|
|
<span class="badge @GetAccessBadgeCss(report)" title="@GetAccessHelp(report)">
|
|
@GetAccessLabel(report)
|
|
</span>
|
|
</td>
|
|
<td class="inbox-tracking-cell">
|
|
<span class="badge @GetTrackingBadgeCss(report)" title="@GetTrackingHelp(report)">@GetTrackingLabel(report)</span>
|
|
</td>
|
|
<td class="inbox-action-cell">
|
|
<button type="button"
|
|
class="btn btn-outline-secondary btn-sm inbox-detail-button"
|
|
title="@GetReportDetailTitle(report)"
|
|
@onclick="@(() => OpenReportDetailAsync(report))"
|
|
disabled="@(DetailBusy || report.Accessible == false)">
|
|
Detalle
|
|
</button>
|
|
</td>
|
|
</tr>
|
|
}
|
|
}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
@if (DetailModalVisible)
|
|
{
|
|
<div class="report-detail-overlay" role="dialog" aria-modal="true" aria-labelledby="report-detail-title">
|
|
<div class="report-detail-dialog">
|
|
<div class="report-detail-card">
|
|
<div class="report-detail-header">
|
|
<div>
|
|
<h5 id="report-detail-title" class="mb-0">Contenido de denuncia #@(DetailReport?.Progressive ?? 0)</h5>
|
|
<small class="text-muted">Mensajes y ficheros comparados contra el ultimo acceso registrado.</small>
|
|
</div>
|
|
<button type="button" class="btn-close report-detail-close" aria-label="Cerrar" @onclick="CloseReportDetail"></button>
|
|
</div>
|
|
<div class="report-detail-body">
|
|
<div class="alert alert-warning small">
|
|
Esta consulta abre el detalle en GlobalLeaks y puede marcar la denuncia como leida.
|
|
</div>
|
|
|
|
@if (DetailBusy)
|
|
{
|
|
<div class="text-muted">Cargando detalle de GlobalLeaks...</div>
|
|
}
|
|
else if (!string.IsNullOrWhiteSpace(DetailError))
|
|
{
|
|
<div class="alert alert-danger">@DetailError</div>
|
|
}
|
|
else if (DetailData is not null)
|
|
{
|
|
<p class="small text-muted">
|
|
Ultimo acceso previo:
|
|
@(string.IsNullOrWhiteSpace(DetailData.LastAccess) ? "Sin acceso previo" : FormatDate(DetailData.LastAccess)).
|
|
Los elementos posteriores se marcan como nuevos.
|
|
</p>
|
|
|
|
<h6 class="text-uppercase text-muted small fw-bold mt-4">Mensajes (@DetailData.Comments.Count)</h6>
|
|
@if (DetailData.Comments.Count == 0)
|
|
{
|
|
<p class="text-muted small">Sin mensajes.</p>
|
|
}
|
|
else
|
|
{
|
|
@foreach (var comment in DetailData.Comments)
|
|
{
|
|
<div class="border rounded p-3 mb-2 @(comment.IsNew ? "border-success bg-success-subtle" : "bg-light")">
|
|
<div class="d-flex flex-column flex-md-row justify-content-between gap-2 small text-muted mb-2">
|
|
<strong>@GetCommentAuthorLabel(comment)</strong>
|
|
<span>@FormatDate(comment.CreationDate)</span>
|
|
</div>
|
|
@if (comment.IsNew)
|
|
{
|
|
<span class="badge bg-success mb-2">Nuevo</span>
|
|
}
|
|
<div style="white-space: pre-wrap;">@(string.IsNullOrWhiteSpace(comment.Content) ? "-" : comment.Content)</div>
|
|
</div>
|
|
}
|
|
}
|
|
|
|
<h6 class="text-uppercase text-muted small fw-bold mt-4">Ficheros del denunciante (@DetailData.WhistleblowerFiles.Count)</h6>
|
|
@if (DetailData.WhistleblowerFiles.Count == 0)
|
|
{
|
|
<p class="text-muted small">Sin ficheros.</p>
|
|
}
|
|
else
|
|
{
|
|
@foreach (var file in DetailData.WhistleblowerFiles)
|
|
{
|
|
<div class="border rounded p-3 mb-2 @(file.IsNew ? "border-success bg-success-subtle" : "bg-light")">
|
|
<div class="d-flex flex-column flex-md-row justify-content-between gap-2">
|
|
<strong>@(string.IsNullOrWhiteSpace(file.Name) ? "Fichero sin nombre" : file.Name)</strong>
|
|
<span class="small text-muted">@FormatDate(file.CreationDate)</span>
|
|
</div>
|
|
<div class="small text-muted">@FormatBytes(file.Size) @(string.IsNullOrWhiteSpace(file.ContentType) ? string.Empty : $" - {file.ContentType}")</div>
|
|
@if (file.IsNew)
|
|
{
|
|
<span class="badge bg-success mt-2">Nuevo</span>
|
|
}
|
|
</div>
|
|
}
|
|
}
|
|
|
|
<h6 class="text-uppercase text-muted small fw-bold mt-4">Ficheros internos/receptor (@DetailData.ReceiverFiles.Count)</h6>
|
|
@if (DetailData.ReceiverFiles.Count == 0)
|
|
{
|
|
<p class="text-muted small">Sin ficheros.</p>
|
|
}
|
|
else
|
|
{
|
|
@foreach (var file in DetailData.ReceiverFiles)
|
|
{
|
|
<div class="border rounded p-3 mb-2 @(file.IsNew ? "border-success bg-success-subtle" : "bg-light")">
|
|
<div class="d-flex flex-column flex-md-row justify-content-between gap-2">
|
|
<strong>@(string.IsNullOrWhiteSpace(file.Name) ? "Fichero sin nombre" : file.Name)</strong>
|
|
<span class="small text-muted">@FormatDate(file.CreationDate)</span>
|
|
</div>
|
|
<div class="small text-muted">@FormatBytes(file.Size) @(string.IsNullOrWhiteSpace(file.ContentType) ? string.Empty : $" - {file.ContentType}")</div>
|
|
@if (!string.IsNullOrWhiteSpace(file.AuthorName))
|
|
{
|
|
<div class="small text-muted">Añadido por @file.AuthorName</div>
|
|
}
|
|
@if (file.IsNew)
|
|
{
|
|
<span class="badge bg-success mt-2">Nuevo</span>
|
|
}
|
|
</div>
|
|
}
|
|
}
|
|
}
|
|
</div>
|
|
<div class="report-detail-footer">
|
|
<button type="button" class="btn btn-secondary" @onclick="CloseReportDetail">Cerrar</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
}
|
|
|
|
@code {
|
|
private readonly List<ContextDto> Contexts = [];
|
|
private readonly List<ReportDto> Reports = [];
|
|
private List<ReportDto> VisibleReports = [];
|
|
private HashSet<string> SelectedIds = [];
|
|
|
|
private ApiGlobalLeaksSessionDto? SessionInfo;
|
|
private InboxUserState UserInboxState = new();
|
|
private string CurrentUsername { get; set; } = string.Empty;
|
|
private string RenewAuthcode { get; set; } = string.Empty;
|
|
private string RenewPendingLoginId { get; set; } = string.Empty;
|
|
private string Filter { get; set; } = "all";
|
|
private string DateScope { get; set; } = "all";
|
|
private string DateFrom { get; set; } = string.Empty;
|
|
private string DateTo { get; set; } = string.Empty;
|
|
private string SelectedChannel { get; set; } = string.Empty;
|
|
private string SearchTerm { get; set; } = string.Empty;
|
|
private string StatusMessage { get; set; } = string.Empty;
|
|
private string StatusCss { get; set; } = "alert-info";
|
|
private string FilterWarningMessage { get; set; } = string.Empty;
|
|
private string FilterWarningCss { get; set; } = "alert-warning";
|
|
private bool ReportsBusy { get; set; }
|
|
private bool RenewBusy { get; set; }
|
|
private bool ImportBusy { get; set; }
|
|
private bool DetailBusy { get; set; }
|
|
private bool DetailModalVisible { get; set; }
|
|
private string DetailError { get; set; } = string.Empty;
|
|
private ReportDto? DetailReport { get; set; }
|
|
private ReportDetailDto? DetailData { get; set; }
|
|
private readonly Dictionary<string, ReportDetailDto> DetailCache = new(StringComparer.OrdinalIgnoreCase);
|
|
|
|
private bool CanUseGlobalLeaks => SessionInfo?.HasActiveSession == true;
|
|
private bool RenewPrepared => !string.IsNullOrWhiteSpace(RenewPendingLoginId);
|
|
private int SelectedReportsCount => Reports.Count(report => SelectedIds.Contains(report.Id) && CanUseReport(report));
|
|
private bool CanImportSelected => CanUseGlobalLeaks && SelectedReportsCount > 0 && !ImportBusy;
|
|
private string SessionStatusText => SessionInfo is null
|
|
? "Sin credenciales guardadas"
|
|
: SessionInfo.HasActiveSession
|
|
? "Activa"
|
|
: "Pendiente de renovacion 2FA";
|
|
private string SessionRenewButtonText => RenewPrepared
|
|
? "Validar 2FA y activar"
|
|
: "Preparar renovacion";
|
|
private string SessionRenewBusyText => RenewPrepared
|
|
? "Validando 2FA..."
|
|
: "Preparando...";
|
|
private string SelectAllLabel => VisibleReports.Any(CanUseReport) && VisibleReports.Where(CanUseReport).All(report => SelectedIds.Contains(report.Id))
|
|
? "Deseleccionar todas"
|
|
: "Seleccionar todas";
|
|
|
|
protected override async Task OnInitializedAsync()
|
|
{
|
|
var authState = await AuthenticationStateProvider.GetAuthenticationStateAsync();
|
|
CurrentUsername = authState.User.Identity?.Name ?? string.Empty;
|
|
|
|
await LoadSessionStateAsync();
|
|
|
|
if (CanUseGlobalLeaks)
|
|
{
|
|
await LoadReportsAsync();
|
|
}
|
|
else if (SessionInfo is not null)
|
|
{
|
|
SetStatus(
|
|
"La aplicacion sigue iniciada, pero la sesion de GlobalLeaks no esta activa. Introduce un nuevo 2FA para renovarla.",
|
|
"alert-warning");
|
|
}
|
|
}
|
|
|
|
private async Task LoadSessionStateAsync()
|
|
{
|
|
SessionInfo = string.IsNullOrWhiteSpace(CurrentUsername)
|
|
? null
|
|
: await ApiDenuncias.GetGlobalLeaksSessionAsync();
|
|
|
|
if (CanUseGlobalLeaks)
|
|
{
|
|
RenewPendingLoginId = string.Empty;
|
|
RenewAuthcode = string.Empty;
|
|
}
|
|
}
|
|
|
|
private async Task RenewSessionAsync()
|
|
{
|
|
if (SessionInfo is null)
|
|
{
|
|
SetStatus("No hay credenciales guardadas para este usuario. Cierra sesion y vuelve a entrar.", "alert-danger");
|
|
return;
|
|
}
|
|
|
|
if (!RenewPrepared)
|
|
{
|
|
await PrepareRenewSessionAsync();
|
|
return;
|
|
}
|
|
|
|
if (string.IsNullOrWhiteSpace(RenewAuthcode) || RenewAuthcode.Trim().Length != 6)
|
|
{
|
|
SetStatus("Introduce un codigo 2FA valido de 6 digitos.", "alert-warning");
|
|
return;
|
|
}
|
|
|
|
RenewBusy = true;
|
|
try
|
|
{
|
|
using var busy = Busy.Show(
|
|
"Validando 2FA",
|
|
"Completando la renovacion con el codigo actual de GlobalLeaks.");
|
|
|
|
SessionInfo = await ApiDenuncias.RenewGlobalLeaksSessionAsync(
|
|
RenewAuthcode.Trim(),
|
|
RenewPendingLoginId,
|
|
CancellationToken.None);
|
|
RenewAuthcode = string.Empty;
|
|
RenewPendingLoginId = string.Empty;
|
|
Busy.Update(message: "Sesion renovada. Actualizando la bandeja de entrada.");
|
|
await LoadReportsAsync();
|
|
SetStatus("Sesion de GlobalLeaks renovada correctamente.", "alert-success");
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
SetStatus(ex.Message, "alert-danger");
|
|
}
|
|
finally
|
|
{
|
|
RenewBusy = false;
|
|
}
|
|
}
|
|
|
|
private async Task PrepareRenewSessionAsync()
|
|
{
|
|
RenewBusy = true;
|
|
try
|
|
{
|
|
using var busy = Busy.Show(
|
|
"Preparando renovacion",
|
|
"Validando credenciales guardadas y preparando GlobalLeaks antes de pedir el codigo 2FA.");
|
|
|
|
var prepared = await ApiDenuncias.PrepareGlobalLeaksSessionRenewalAsync(CancellationToken.None);
|
|
RenewPendingLoginId = prepared.PendingLoginId;
|
|
RenewAuthcode = string.Empty;
|
|
SetStatus("Renovacion preparada. Introduce ahora el codigo 2FA que este activo.", "alert-info");
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
SetStatus(ex.Message, "alert-danger");
|
|
}
|
|
finally
|
|
{
|
|
RenewBusy = false;
|
|
}
|
|
}
|
|
|
|
private async Task LoadReportsAsync()
|
|
{
|
|
if (!CanUseGlobalLeaks)
|
|
{
|
|
return;
|
|
}
|
|
|
|
ReportsBusy = true;
|
|
try
|
|
{
|
|
using var busy = Busy.Show(
|
|
"Actualizando bandeja",
|
|
"Consultando GlobalLeaks y cruzando el seguimiento con la base de datos.");
|
|
|
|
var inbox = await ApiDenuncias.LoadInboxAsync(CancellationToken.None);
|
|
|
|
Contexts.Clear();
|
|
Contexts.AddRange(inbox.Contexts);
|
|
|
|
Reports.Clear();
|
|
Reports.AddRange(inbox.Reports);
|
|
DetailCache.Clear();
|
|
UserInboxState = inbox.UserState;
|
|
ApplyFilters();
|
|
}
|
|
catch (UnauthorizedAccessException ex)
|
|
{
|
|
await LoadSessionStateAsync();
|
|
Reports.Clear();
|
|
VisibleReports.Clear();
|
|
SelectedIds.Clear();
|
|
SetStatus(ex.Message, "alert-warning");
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
SetStatus(ex.Message, "alert-danger");
|
|
}
|
|
finally
|
|
{
|
|
ReportsBusy = false;
|
|
}
|
|
}
|
|
|
|
private async Task ImportSelectedAsync()
|
|
{
|
|
if (!CanUseGlobalLeaks)
|
|
{
|
|
SetStatus("Renueva antes la sesion de GlobalLeaks para importar denuncias.", "alert-warning");
|
|
return;
|
|
}
|
|
|
|
var selectedReports = Reports
|
|
.Where(report => SelectedIds.Contains(report.Id))
|
|
.Where(CanUseReport)
|
|
.OrderBy(report => report.Progressive ?? 0)
|
|
.ToList();
|
|
|
|
if (selectedReports.Count == 0)
|
|
{
|
|
SetStatus("No hay denuncias accesibles seleccionadas para importar.", "alert-warning");
|
|
return;
|
|
}
|
|
|
|
var reportsFromOtherOwners = selectedReports
|
|
.Where(report => report.RequiresOwnerConfirmation)
|
|
.ToArray();
|
|
if (reportsFromOtherOwners.Length > 0)
|
|
{
|
|
var ownerSummary = string.Join(
|
|
Environment.NewLine,
|
|
reportsFromOtherOwners.Select(report =>
|
|
$"Denuncia #{report.Progressive ?? 0}: propiedad de {report.OwnerUsername}."));
|
|
var confirmed = await Dialogs.ConfirmAsync(
|
|
"Denuncias asignadas a otros usuarios",
|
|
$"Vas a importar denuncias asignadas a otros usuarios de tu grupo:{Environment.NewLine}{Environment.NewLine}{ownerSummary}{Environment.NewLine}{Environment.NewLine}La importación quedará registrada con tu usuario.",
|
|
"Importar denuncias");
|
|
if (!confirmed)
|
|
{
|
|
return;
|
|
}
|
|
}
|
|
|
|
ImportBusy = true;
|
|
var importedCount = 0;
|
|
var errors = new List<string>();
|
|
var importWarnings = new List<string>();
|
|
|
|
try
|
|
{
|
|
using var busy = Busy.Show(
|
|
"Importando denuncias",
|
|
$"Descargando y procesando {selectedReports.Count} denuncia(s) desde GlobalLeaks.",
|
|
selectedReports.Count);
|
|
|
|
var processed = 0;
|
|
foreach (var report in selectedReports)
|
|
{
|
|
processed++;
|
|
Busy.Update(
|
|
message: $"Procesando denuncia #{report.Progressive ?? 0}.",
|
|
detail: "Descarga, analisis del report y guardado seguro en la base de datos.",
|
|
current: processed,
|
|
total: selectedReports.Count);
|
|
|
|
try
|
|
{
|
|
var result = await ApiDenuncias.ImportReportAsync(
|
|
report,
|
|
report.RequiresOwnerConfirmation,
|
|
CancellationToken.None);
|
|
importedCount += result.ImportedCount;
|
|
errors.AddRange(result.Errors.Select(error => $"#{report.Progressive ?? 0}: {error}"));
|
|
if (result.Warnings is not null)
|
|
{
|
|
importWarnings.AddRange(result.Warnings.Select(warning => $"#{report.Progressive ?? 0}: {warning}"));
|
|
}
|
|
}
|
|
catch (UnauthorizedAccessException ex)
|
|
{
|
|
await LoadSessionStateAsync();
|
|
SetStatus(ex.Message, "alert-warning");
|
|
break;
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
errors.Add($"#{report.Progressive ?? 0}: {ex.Message}");
|
|
}
|
|
}
|
|
|
|
SelectedIds.Clear();
|
|
await LoadReportsAsync();
|
|
|
|
var warnings = selectedReports
|
|
.Where(report => report.DownloadedByAnotherUser || report.AlreadyInGestiona)
|
|
.Select(report => $"#{report.Progressive ?? 0}: {report.TrackingNote}")
|
|
.Where(message => !string.IsNullOrWhiteSpace(message))
|
|
.ToList();
|
|
warnings.AddRange(importWarnings);
|
|
|
|
if (errors.Count == 0 && warnings.Count == 0)
|
|
{
|
|
SetStatus($"Se han importado {importedCount} denuncia(s) desde GlobalLeaks.", "alert-success");
|
|
}
|
|
else if (errors.Count == 0 && importedCount == 0)
|
|
{
|
|
var parts = new List<string>
|
|
{
|
|
"No se ha incorporado ninguna denuncia nueva."
|
|
};
|
|
|
|
if (warnings.Count > 0)
|
|
{
|
|
parts.Add($"Avisos: {string.Join(" | ", warnings)}");
|
|
}
|
|
|
|
SetStatus(string.Join(" ", parts), "alert-warning");
|
|
}
|
|
else
|
|
{
|
|
var parts = new List<string>
|
|
{
|
|
$"Se han importado {importedCount} denuncia(s)."
|
|
};
|
|
|
|
if (warnings.Count > 0)
|
|
{
|
|
parts.Add($"Avisos: {string.Join(" | ", warnings)}");
|
|
}
|
|
|
|
if (errors.Count > 0)
|
|
{
|
|
parts.Add($"Incidencias: {string.Join(" | ", errors)}");
|
|
}
|
|
|
|
SetStatus(string.Join(" ", parts), errors.Count == 0 ? "alert-warning" : "alert-danger");
|
|
}
|
|
}
|
|
finally
|
|
{
|
|
ImportBusy = false;
|
|
}
|
|
}
|
|
|
|
private async Task OpenReportDetailAsync(ReportDto report)
|
|
{
|
|
if (report.Accessible == false)
|
|
{
|
|
SetStatus($"La denuncia #{report.Progressive ?? 0} no es accesible para este usuario en GlobalLeaks.", "alert-warning");
|
|
return;
|
|
}
|
|
|
|
if (!CanUseGlobalLeaks)
|
|
{
|
|
SetStatus("Renueva antes la sesion de GlobalLeaks para consultar el detalle.", "alert-warning");
|
|
return;
|
|
}
|
|
|
|
DetailModalVisible = true;
|
|
DetailReport = report;
|
|
DetailData = null;
|
|
DetailError = string.Empty;
|
|
DetailBusy = true;
|
|
await SetBodyScrollLockAsync(true);
|
|
|
|
try
|
|
{
|
|
using var busy = Busy.Show(
|
|
"Leyendo detalle de denuncia",
|
|
"Consultando mensajes y ficheros para identificar que contenido es nuevo.");
|
|
|
|
if (!DetailCache.TryGetValue(report.Id, out var cachedDetail))
|
|
{
|
|
cachedDetail = await ApiDenuncias.GetReportDetailAsync(report.Id, report.LastAccess, CancellationToken.None);
|
|
DetailCache[report.Id] = cachedDetail;
|
|
}
|
|
|
|
DetailData = cachedDetail;
|
|
}
|
|
catch (UnauthorizedAccessException ex)
|
|
{
|
|
await LoadSessionStateAsync();
|
|
DetailError = ex.Message;
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
DetailError = ex.Message;
|
|
}
|
|
finally
|
|
{
|
|
DetailBusy = false;
|
|
}
|
|
}
|
|
|
|
private async Task CloseReportDetail()
|
|
{
|
|
DetailModalVisible = false;
|
|
DetailReport = null;
|
|
DetailData = null;
|
|
DetailError = string.Empty;
|
|
await SetBodyScrollLockAsync(false);
|
|
}
|
|
|
|
public async ValueTask DisposeAsync()
|
|
{
|
|
await SetBodyScrollLockAsync(false);
|
|
}
|
|
|
|
private async Task SetBodyScrollLockAsync(bool locked)
|
|
{
|
|
try
|
|
{
|
|
await JSRuntime.InvokeVoidAsync("appSetBodyScrollLock", locked);
|
|
}
|
|
catch
|
|
{
|
|
// Si el circuito se esta cerrando, solo aseguramos que no reviente el componente.
|
|
}
|
|
}
|
|
|
|
private void ApplyFilters()
|
|
{
|
|
IEnumerable<ReportDto> filtered = Reports;
|
|
FilterWarningMessage = string.Empty;
|
|
FilterWarningCss = "alert-warning";
|
|
|
|
filtered = Filter switch
|
|
{
|
|
"new" => filtered.Where(report => string.IsNullOrWhiteSpace(report.AccessDate) || string.Equals(report.Status, "new", StringComparison.OrdinalIgnoreCase)),
|
|
"updated" => filtered.Where(report =>
|
|
report.CitizenHasNewActivity ||
|
|
(!report.ActivityAnalyzed && report.Updated)),
|
|
"receiver" => filtered.Where(report => report.ReceiverHasNewActivity),
|
|
_ => filtered
|
|
};
|
|
|
|
filtered = ApplyDateScope(filtered);
|
|
|
|
if (!string.IsNullOrWhiteSpace(SelectedChannel))
|
|
{
|
|
filtered = filtered.Where(report => string.Equals(report.ContextId, SelectedChannel, StringComparison.OrdinalIgnoreCase));
|
|
}
|
|
|
|
if (!string.IsNullOrWhiteSpace(SearchTerm))
|
|
{
|
|
var search = SearchTerm.Trim();
|
|
filtered = filtered.Where(report =>
|
|
(report.Progressive?.ToString() ?? string.Empty).Contains(search, StringComparison.OrdinalIgnoreCase) ||
|
|
(report.ContextName ?? report.ContextId ?? string.Empty).Contains(search, StringComparison.OrdinalIgnoreCase));
|
|
}
|
|
|
|
VisibleReports = filtered
|
|
.OrderByDescending(report => report.Progressive ?? 0)
|
|
.ToList();
|
|
|
|
var validIds = Reports
|
|
.Where(CanUseReport)
|
|
.Select(report => report.Id)
|
|
.ToHashSet(StringComparer.OrdinalIgnoreCase);
|
|
SelectedIds.RemoveWhere(id => !validIds.Contains(id));
|
|
}
|
|
|
|
private IEnumerable<ReportDto> ApplyDateScope(IEnumerable<ReportDto> reports)
|
|
{
|
|
if (DateScope == "since_mine")
|
|
{
|
|
if (UserInboxState.LastDownloadedReportMomentUtc is null)
|
|
{
|
|
FilterWarningMessage = "Este usuario aun no tiene descargas previas. Se muestran todas las denuncias del filtro seleccionado.";
|
|
FilterWarningCss = "alert-info";
|
|
return reports;
|
|
}
|
|
|
|
var lastMoment = UserInboxState.LastDownloadedReportMomentUtc.Value;
|
|
return reports.Where(report => GetEffectiveMoment(report) is DateTimeOffset moment && moment > lastMoment);
|
|
}
|
|
|
|
if (DateScope == "range")
|
|
{
|
|
var hasFrom = DateOnly.TryParseExact(DateFrom, "yyyy-MM-dd", CultureInfo.InvariantCulture, DateTimeStyles.None, out var fromDate);
|
|
var hasTo = DateOnly.TryParseExact(DateTo, "yyyy-MM-dd", CultureInfo.InvariantCulture, DateTimeStyles.None, out var toDate);
|
|
|
|
if (hasFrom && hasTo && fromDate > toDate)
|
|
{
|
|
FilterWarningMessage = "La fecha inicial no puede ser posterior a la final.";
|
|
FilterWarningCss = "alert-danger";
|
|
return Enumerable.Empty<ReportDto>();
|
|
}
|
|
|
|
var filtered = reports.Where(report =>
|
|
{
|
|
var moment = GetEffectiveMoment(report);
|
|
if (moment is null)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
var reportDate = DateOnly.FromDateTime(moment.Value.UtcDateTime);
|
|
var matchesFrom = !hasFrom || reportDate >= fromDate;
|
|
var matchesTo = !hasTo || reportDate <= toDate;
|
|
return matchesFrom && matchesTo;
|
|
});
|
|
|
|
if (hasFrom &&
|
|
UserInboxState.LastDownloadedReportMomentUtc is DateTimeOffset lastDownloaded &&
|
|
fromDate > DateOnly.FromDateTime(lastDownloaded.UtcDateTime))
|
|
{
|
|
var gapExists = Reports.Any(report =>
|
|
{
|
|
var moment = GetEffectiveMoment(report);
|
|
if (moment is null)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
var reportDate = DateOnly.FromDateTime(moment.Value.UtcDateTime);
|
|
return moment.Value > lastDownloaded && reportDate < fromDate;
|
|
});
|
|
|
|
if (gapExists)
|
|
{
|
|
FilterWarningMessage =
|
|
$"Ojo: entre tu ultima descarga ({lastDownloaded.ToLocalTime():dd/MM/yyyy HH:mm}) y el inicio del intervalo ({fromDate:dd/MM/yyyy}) hay denuncias o actualizaciones sin revisar.";
|
|
}
|
|
}
|
|
|
|
return filtered;
|
|
}
|
|
|
|
return reports;
|
|
}
|
|
|
|
private void ToggleSelection(ReportDto report, ChangeEventArgs args)
|
|
{
|
|
if (report.Accessible == false)
|
|
{
|
|
SelectedIds.Remove(report.Id);
|
|
StateHasChanged();
|
|
return;
|
|
}
|
|
|
|
var isChecked = GetCheckedValue(args);
|
|
if (isChecked)
|
|
{
|
|
SelectedIds.Add(report.Id);
|
|
}
|
|
else
|
|
{
|
|
SelectedIds.Remove(report.Id);
|
|
}
|
|
|
|
StateHasChanged();
|
|
}
|
|
|
|
private void ToggleSelectAll()
|
|
{
|
|
var selectableReports = VisibleReports
|
|
.Where(CanUseReport)
|
|
.ToList();
|
|
var shouldSelect = selectableReports.Count > 0 && !selectableReports.All(report => SelectedIds.Contains(report.Id));
|
|
|
|
foreach (var report in selectableReports)
|
|
{
|
|
if (shouldSelect)
|
|
{
|
|
SelectedIds.Add(report.Id);
|
|
}
|
|
else
|
|
{
|
|
SelectedIds.Remove(report.Id);
|
|
}
|
|
}
|
|
|
|
StateHasChanged();
|
|
}
|
|
|
|
private void OnFilterChanged(ChangeEventArgs args)
|
|
{
|
|
Filter = args.Value?.ToString() ?? "all";
|
|
ApplyFilters();
|
|
}
|
|
|
|
private void OnDateScopeChanged(ChangeEventArgs args)
|
|
{
|
|
DateScope = args.Value?.ToString() ?? "all";
|
|
if (DateScope != "range")
|
|
{
|
|
DateFrom = string.Empty;
|
|
DateTo = string.Empty;
|
|
}
|
|
|
|
ApplyFilters();
|
|
}
|
|
|
|
private void OnDateFromChanged(ChangeEventArgs args)
|
|
{
|
|
DateFrom = args.Value?.ToString() ?? string.Empty;
|
|
ApplyFilters();
|
|
}
|
|
|
|
private void OnDateToChanged(ChangeEventArgs args)
|
|
{
|
|
DateTo = args.Value?.ToString() ?? string.Empty;
|
|
ApplyFilters();
|
|
}
|
|
|
|
private void OnChannelChanged(ChangeEventArgs args)
|
|
{
|
|
SelectedChannel = args.Value?.ToString() ?? string.Empty;
|
|
ApplyFilters();
|
|
}
|
|
|
|
private void OnSearchChanged(ChangeEventArgs args)
|
|
{
|
|
SearchTerm = args.Value?.ToString() ?? string.Empty;
|
|
ApplyFilters();
|
|
}
|
|
|
|
private void SetStatus(string message, string cssClass)
|
|
{
|
|
StatusMessage = message;
|
|
StatusCss = cssClass;
|
|
}
|
|
|
|
private static bool GetCheckedValue(ChangeEventArgs args)
|
|
{
|
|
return args.Value switch
|
|
{
|
|
bool b => b,
|
|
string s when bool.TryParse(s, out var parsed) => parsed,
|
|
_ => false
|
|
};
|
|
}
|
|
|
|
private static string FormatDate(string? value)
|
|
{
|
|
return FormatOptionalDate(value) ?? "-";
|
|
}
|
|
|
|
private static string? FormatOptionalDate(string? value)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(value))
|
|
{
|
|
return null;
|
|
}
|
|
|
|
return DateTimeOffset.TryParse(value, out var parsed)
|
|
? parsed.ToLocalTime().ToString("dd/MM/yyyy HH:mm")
|
|
: null;
|
|
}
|
|
|
|
private static string FormatCitizenActivity(ReportDto report)
|
|
{
|
|
var citizenDate = FormatOptionalDate(report.CitizenLastActivity);
|
|
if (!string.IsNullOrWhiteSpace(citizenDate))
|
|
{
|
|
return citizenDate;
|
|
}
|
|
|
|
if (!report.ActivityAnalyzed)
|
|
{
|
|
return "No analizada";
|
|
}
|
|
|
|
var accessDate = FormatOptionalDate(report.WhistleblowerLastAccess);
|
|
if (!string.IsNullOrWhiteSpace(accessDate))
|
|
{
|
|
return accessDate;
|
|
}
|
|
|
|
return "Sin fecha";
|
|
}
|
|
|
|
private static string FormatReceiverActivity(ReportDto report)
|
|
{
|
|
var receiverDate = FormatOptionalDate(report.ReceiverLastActivity);
|
|
if (!string.IsNullOrWhiteSpace(receiverDate))
|
|
{
|
|
return string.IsNullOrWhiteSpace(report.ReceiverLastActivityAuthorName)
|
|
? receiverDate
|
|
: $"{receiverDate} · {report.ReceiverLastActivityAuthorName}";
|
|
}
|
|
|
|
if (!report.ActivityAnalyzed)
|
|
{
|
|
return "No analizada";
|
|
}
|
|
|
|
return "Sin actividad";
|
|
}
|
|
|
|
private static string GetReceiverActivityTitle(ReportDto report)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(report.ReceiverLastActivityAuthorName))
|
|
{
|
|
return FormatReceiverActivity(report);
|
|
}
|
|
|
|
return $"Ultima actividad OAAF realizada por {report.ReceiverLastActivityAuthorName}.";
|
|
}
|
|
|
|
private static string FormatBytes(long? value)
|
|
{
|
|
if (value is null or <= 0)
|
|
{
|
|
return "Tamano no disponible";
|
|
}
|
|
|
|
var bytes = value.Value;
|
|
if (bytes < 1024)
|
|
{
|
|
return $"{bytes} B";
|
|
}
|
|
|
|
if (bytes < 1024 * 1024)
|
|
{
|
|
return $"{bytes / 1024d:0.#} KB";
|
|
}
|
|
|
|
return $"{bytes / 1024d / 1024d:0.#} MB";
|
|
}
|
|
|
|
private static string GetCommentAuthorLabel(ReportCommentDto comment)
|
|
=> IsWhistleblowerActivityType(comment.Type)
|
|
? "Denunciante"
|
|
: IsReceiverActivityType(comment.Type)
|
|
? string.IsNullOrWhiteSpace(comment.AuthorName)
|
|
? "Gestor OAAF"
|
|
: $"Gestor OAAF: {comment.AuthorName}"
|
|
: "Comentario";
|
|
|
|
private static bool IsWhistleblowerActivityType(string? value)
|
|
=> MatchesAny(value, "whistleblower", "citizen", "source", "tipper", "submitter", "denunciante");
|
|
|
|
private static bool IsReceiverActivityType(string? value)
|
|
=> MatchesAny(value, "receiver", "recipient", "admin", "administrator", "operator", "staff", "custodian", "moderator", "gestor", "oaaf");
|
|
|
|
private static bool MatchesAny(string? value, params string[] candidates)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(value))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
var normalized = NormalizeActivityType(value);
|
|
return candidates.Any(candidate =>
|
|
{
|
|
var candidateToken = NormalizeActivityType(candidate);
|
|
return normalized == candidateToken ||
|
|
normalized.StartsWith(candidateToken + " ", StringComparison.Ordinal) ||
|
|
normalized.EndsWith(" " + candidateToken, StringComparison.Ordinal) ||
|
|
normalized.Contains(" " + candidateToken + " ", StringComparison.Ordinal);
|
|
});
|
|
}
|
|
|
|
private static string NormalizeActivityType(string value)
|
|
=> Regex.Replace(value.Trim().ToLowerInvariant(), @"[^a-z0-9]+", " ").Trim();
|
|
|
|
private static DateTimeOffset? GetEffectiveMoment(ReportDto report)
|
|
{
|
|
return ParseDate(report.CitizenLastActivity) ?? ParseDate(report.ReceiverLastActivity) ?? ParseDate(report.UpdateDate) ?? ParseDate(report.CreationDate);
|
|
}
|
|
|
|
private static DateTimeOffset? ParseDate(string? value)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(value))
|
|
{
|
|
return null;
|
|
}
|
|
|
|
return DateTimeOffset.TryParse(value.Replace("Z", "+00:00", StringComparison.Ordinal), out var parsed)
|
|
? parsed
|
|
: null;
|
|
}
|
|
|
|
private static string GetStatusLabel(ReportDto report)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(report.AccessDate))
|
|
{
|
|
return "Sin leer";
|
|
}
|
|
|
|
if (report.CitizenHasNewActivity)
|
|
{
|
|
return "Actualización ciudadano";
|
|
}
|
|
|
|
if (IsReceiverOnlyUpdate(report))
|
|
{
|
|
return "Actividad OAAF";
|
|
}
|
|
|
|
if (report.AlreadyInGestiona && !report.ActivityAnalyzed)
|
|
{
|
|
return "Sin comprobar";
|
|
}
|
|
|
|
if (IsUpToDateInGestiona(report))
|
|
{
|
|
return "Actualizada";
|
|
}
|
|
|
|
return string.Equals(report.Status, "closed", StringComparison.OrdinalIgnoreCase)
|
|
? "Cerrada"
|
|
: "Nueva denuncia";
|
|
}
|
|
|
|
private static string GetStatusBadgeCss(ReportDto report)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(report.AccessDate))
|
|
{
|
|
return "bg-warning text-dark";
|
|
}
|
|
|
|
if (report.CitizenHasNewActivity)
|
|
{
|
|
return "bg-info text-dark";
|
|
}
|
|
|
|
if (IsReceiverOnlyUpdate(report))
|
|
{
|
|
return "bg-secondary";
|
|
}
|
|
|
|
if (report.AlreadyInGestiona && !report.ActivityAnalyzed)
|
|
{
|
|
return "bg-warning text-dark";
|
|
}
|
|
|
|
if (IsUpToDateInGestiona(report))
|
|
{
|
|
return "bg-light text-dark";
|
|
}
|
|
|
|
return string.Equals(report.Status, "closed", StringComparison.OrdinalIgnoreCase)
|
|
? "bg-secondary"
|
|
: "bg-primary";
|
|
}
|
|
|
|
private static bool IsUpToDateInGestiona(ReportDto report)
|
|
{
|
|
if (!report.AlreadyInGestiona)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
var lastUpload = ParseDate(report.LastGestionaUploadAt);
|
|
var latestActivity = new[]
|
|
{
|
|
ParseDate(report.CitizenLastActivity),
|
|
ParseDate(report.ReceiverLastActivity)
|
|
}
|
|
.Where(value => value is not null)
|
|
.Select(value => value!.Value)
|
|
.DefaultIfEmpty()
|
|
.Max();
|
|
|
|
if (lastUpload is not null && latestActivity != default)
|
|
{
|
|
return lastUpload.Value >= latestActivity;
|
|
}
|
|
|
|
return report.ActivityAnalyzed &&
|
|
!report.CitizenHasNewActivity &&
|
|
!report.ReceiverHasNewActivity;
|
|
}
|
|
|
|
private static string GetStatusHelp(ReportDto report)
|
|
=> GetStatusLabel(report) switch
|
|
{
|
|
"Nueva denuncia" => "La denuncia todavía no tiene expediente creado desde la aplicación y está pendiente de actuación.",
|
|
"Actualización ciudadano" => "El ciudadano ha añadido un comentario, un fichero o una modificación posterior a la última subida a Gestiona.",
|
|
"Actividad OAAF" => "Un gestor de la OAAF ha realizado una comunicación o añadido un fichero posterior a la última subida a Gestiona.",
|
|
"Actualizada" => "La última subida a Gestiona es igual o posterior a la actividad del ciudadano y de la OAAF detectada en el buzón.",
|
|
"Sin comprobar" => "No se ha podido comparar en este momento la actividad del buzón con la última subida a Gestiona.",
|
|
"Cerrada" => "La denuncia figura cerrada en GlobalLeaks.",
|
|
"Sin leer" => "La denuncia todavía no se ha abierto con este usuario en GlobalLeaks.",
|
|
_ => string.Empty
|
|
};
|
|
|
|
private static string GetAccessLabel(ReportDto report)
|
|
=> report.Accessible switch
|
|
{
|
|
true => "Accesible",
|
|
false => "Sin acceso",
|
|
_ => "Sin dato"
|
|
};
|
|
|
|
private static string GetAccessBadgeCss(ReportDto report)
|
|
=> report.Accessible switch
|
|
{
|
|
true => "bg-success",
|
|
false => "bg-danger",
|
|
_ => "bg-light text-dark"
|
|
};
|
|
|
|
private static string GetAccessHelp(ReportDto report)
|
|
=> report.Accessible switch
|
|
{
|
|
true => "Tu usuario gestor del buzón tiene acceso a esta denuncia.",
|
|
false => "GlobalLeaks indica que tu usuario gestor del buzón no tiene acceso a esta denuncia.",
|
|
_ => "GlobalLeaks no ha informado si tu usuario puede acceder a esta denuncia."
|
|
};
|
|
|
|
private static bool CanUseReport(ReportDto report)
|
|
=> report.Accessible != false;
|
|
|
|
private static bool IsReceiverOnlyUpdate(ReportDto report)
|
|
=> report.AlreadyInGestiona &&
|
|
report.ActivityAnalyzed &&
|
|
!report.CitizenHasNewActivity &&
|
|
report.ReceiverHasNewActivity;
|
|
|
|
private static string? GetReportActionBlockReason(ReportDto report)
|
|
{
|
|
if (report.Accessible == false)
|
|
{
|
|
return "GlobalLeaks indica que esta denuncia no es accesible para este usuario.";
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
private static string GetReportDetailTitle(ReportDto report)
|
|
{
|
|
if (report.Accessible == false)
|
|
{
|
|
return "GlobalLeaks indica que esta denuncia no es accesible para este usuario.";
|
|
}
|
|
|
|
return "Consulta mensajes y ficheros. GlobalLeaks puede marcar la denuncia como leida.";
|
|
}
|
|
|
|
private static string GetTrackingLabel(ReportDto report)
|
|
{
|
|
if (report.AlreadyInGestiona)
|
|
{
|
|
return "Expediente creado";
|
|
}
|
|
|
|
if (report.DownloadedByAnotherUser)
|
|
{
|
|
return "Descargada por otro";
|
|
}
|
|
|
|
if (report.DownloadedByCurrentUser)
|
|
{
|
|
return "Descargada por ti";
|
|
}
|
|
|
|
if (report.AlreadyImported)
|
|
{
|
|
return "Ya incorporada";
|
|
}
|
|
|
|
return "Pendiente";
|
|
}
|
|
|
|
private static string GetTrackingBadgeCss(ReportDto report)
|
|
{
|
|
if (report.AlreadyInGestiona)
|
|
{
|
|
return "bg-success";
|
|
}
|
|
|
|
if (report.DownloadedByAnotherUser)
|
|
{
|
|
return "bg-warning text-dark";
|
|
}
|
|
|
|
if (report.DownloadedByCurrentUser)
|
|
{
|
|
return "bg-secondary";
|
|
}
|
|
|
|
if (report.AlreadyImported)
|
|
{
|
|
return "bg-info 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)
|
|
{
|
|
if (report.Accessible == false)
|
|
{
|
|
return "table-danger report-row-disabled";
|
|
}
|
|
|
|
if (IsReceiverOnlyUpdate(report))
|
|
{
|
|
return "table-secondary";
|
|
}
|
|
|
|
if (report.AlreadyInGestiona)
|
|
{
|
|
return "table-success";
|
|
}
|
|
|
|
if (report.DownloadedByAnotherUser)
|
|
{
|
|
return "table-warning";
|
|
}
|
|
|
|
return null;
|
|
}
|
|
}
|