Files
Antifraude.Net/Antifraude.Net/GestionaDenunciasAN/Components/Pages/GestionZip.razor
2026-05-06 13:48:23 +02:00

771 lines
28 KiB
Plaintext

@page "/GestionZip"
@rendermode @(new InteractiveServerRenderMode(prerender: false))
@attribute [Authorize]
@using System.Globalization
@using GestionaDenunciasAN.Models
@inject AuthenticationStateProvider AuthenticationStateProvider
@inject ApiDenunciasClient ApiDenuncias
<PageTitle>Entrada de denuncias</PageTitle>
<div class="container py-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="row g-4">
<div class="col-lg-4">
<div class="card shadow-sm h-100">
<div class="card-body">
<h5 class="card-title">Sesion GlobalLeaks</h5>
<p class="mb-2"><strong>Usuario:</strong> @CurrentUsername</p>
<p class="mb-2"><strong>Estado:</strong> @SessionStatusText</p>
<p class="mb-2">
<strong>Ultima descarga registrada:</strong>
@(UserInboxState.LastDownloadedReportMomentUtc is null
? "Sin descargas previas"
: UserInboxState.LastDownloadedReportMomentUtc.Value.ToLocalTime().ToString("dd/MM/yyyy HH:mm"))
</p>
<p class="text-muted small mb-4">
La base de datos lleva la cuenta de lo que ya ha descargado este usuario, de lo que han descargado otros
y de si el expediente ya esta creado en Gestiona.
</p>
<div class="mb-3">
<label class="form-label">Nuevo codigo 2FA</label>
<input class="form-control"
@bind="RenewAuthcode"
maxlength="6"
inputmode="numeric"
placeholder="123456" />
</div>
<button type="button" class="btn btn-primary w-100" @onclick="RenewSessionAsync" disabled="@RenewBusy">
@(RenewBusy ? "Renovando..." : SessionRenewButtonText)
</button>
</div>
</div>
</div>
<div class="col-lg-8">
<div class="card shadow-sm">
<div class="card-body">
<div class="d-flex flex-column flex-md-row justify-content-between align-items-md-center gap-3 mb-3">
<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</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 (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())">
@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">
<table class="table table-hover align-middle">
<thead>
<tr>
<th style="width: 3rem;"></th>
<th>#</th>
<th>Canal</th>
<th>Presentacion</th>
<th>Ultima actualizacion</th>
<th>Estado</th>
<th>Seguimiento</th>
</tr>
</thead>
<tbody>
@if (!CanUseGlobalLeaks)
{
<tr>
<td colspan="7" class="text-muted">
Renueva la sesion de GlobalLeaks con un 2FA valido para cargar la bandeja.
</td>
</tr>
}
else if (ReportsBusy)
{
<tr>
<td colspan="7" class="text-muted">Cargando denuncias...</td>
</tr>
}
else if (!VisibleReports.Any())
{
<tr>
<td colspan="7" 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)"
@onchange="@((ChangeEventArgs args) => ToggleSelection(report.Id, args))" />
</td>
<td><strong>#@(report.Progressive ?? 0)</strong></td>
<td>@(report.ContextName ?? report.ContextId ?? "-")</td>
<td>@FormatDate(report.CreationDate)</td>
<td>@FormatDate(report.UpdateDate)</td>
<td>
<span class="badge @GetStatusBadgeCss(report)">
@GetStatusLabel(report)
</span>
</td>
<td>
<span class="badge @GetTrackingBadgeCss(report)">
@GetTrackingLabel(report)
</span>
@if (!string.IsNullOrWhiteSpace(report.TrackingNote))
{
<div class="small text-muted mt-1">@report.TrackingNote</div>
}
</td>
</tr>
}
}
</tbody>
</table>
</div>
</div>
</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 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 CanUseGlobalLeaks => SessionInfo?.HasActiveSession == true;
private int SelectedReportsCount => SelectedIds.Count;
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 => SessionInfo?.HasActiveSession == true
? "Renovar sesion"
: "Activar sesion";
private string SelectAllLabel => VisibleReports.Count > 0 && VisibleReports.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();
}
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 (string.IsNullOrWhiteSpace(RenewAuthcode) || RenewAuthcode.Trim().Length != 6)
{
SetStatus("Introduce un codigo 2FA valido de 6 digitos.", "alert-warning");
return;
}
RenewBusy = true;
try
{
SessionInfo = await ApiDenuncias.RenewGlobalLeaksSessionAsync(RenewAuthcode.Trim(), CancellationToken.None);
RenewAuthcode = string.Empty;
await LoadReportsAsync();
SetStatus("Sesion de GlobalLeaks renovada correctamente.", "alert-success");
}
catch (Exception ex)
{
SetStatus(ex.Message, "alert-danger");
}
finally
{
RenewBusy = false;
}
}
private async Task LoadReportsAsync()
{
if (!CanUseGlobalLeaks)
{
return;
}
ReportsBusy = true;
try
{
var inbox = await ApiDenuncias.LoadInboxAsync(CancellationToken.None);
Contexts.Clear();
Contexts.AddRange(inbox.Contexts);
Reports.Clear();
Reports.AddRange(inbox.Reports);
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;
}
ImportBusy = true;
var importedCount = 0;
var errors = new List<string>();
try
{
var selectedReports = Reports
.Where(report => SelectedIds.Contains(report.Id))
.OrderBy(report => report.Progressive ?? 0)
.ToList();
foreach (var report in selectedReports)
{
try
{
var result = await ApiDenuncias.ImportReportAsync(report, CancellationToken.None);
importedCount += result.ImportedCount;
errors.AddRange(result.Errors.Select(error => $"#{report.Progressive ?? 0}: {error}"));
}
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();
if (errors.Count == 0 && warnings.Count == 0)
{
SetStatus($"Se han importado {importedCount} denuncia(s) desde GlobalLeaks.", "alert-success");
}
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 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.Updated),
_ => 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.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(string reportId, ChangeEventArgs args)
{
var isChecked = GetCheckedValue(args);
if (isChecked)
{
SelectedIds.Add(reportId);
}
else
{
SelectedIds.Remove(reportId);
}
StateHasChanged();
}
private void ToggleSelectAll()
{
var shouldSelect = !VisibleReports.All(report => SelectedIds.Contains(report.Id));
foreach (var report in VisibleReports)
{
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)
{
if (string.IsNullOrWhiteSpace(value))
{
return "-";
}
return DateTimeOffset.TryParse(value, out var parsed)
? parsed.ToLocalTime().ToString("dd/MM/yyyy HH:mm")
: "-";
}
private static DateTimeOffset? GetEffectiveMoment(ReportDto report)
{
return 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.Updated)
{
return "Actualizada";
}
return string.Equals(report.Status, "closed", StringComparison.OrdinalIgnoreCase)
? "Cerrada"
: "Abierta";
}
private static string GetStatusBadgeCss(ReportDto report)
{
if (string.IsNullOrWhiteSpace(report.AccessDate))
{
return "bg-warning text-dark";
}
if (report.Updated)
{
return "bg-info text-dark";
}
return string.Equals(report.Status, "closed", StringComparison.OrdinalIgnoreCase)
? "bg-secondary"
: "bg-primary";
}
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? GetReportRowCss(ReportDto report)
{
if (report.AlreadyInGestiona)
{
return "table-success";
}
if (report.DownloadedByAnotherUser)
{
return "table-warning";
}
return null;
}
}