- Sustituidas las confirmaciones genéricas del navegador por una ventana propia, responsive y accesible.

- 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.
This commit is contained in:
2026-07-30 12:35:50 +02:00
parent 8dedafdc07
commit 2b4678c26d
9 changed files with 373 additions and 24 deletions

View File

@@ -0,0 +1,110 @@
@implements IDisposable
@inject UiDialogService Dialog
@if (Dialog.IsVisible)
{
<div class="app-confirmation-backdrop"
role="presentation"
@onkeydown="HandleKeyDown">
<section class="@DialogCss"
role="alertdialog"
aria-modal="true"
aria-labelledby="app-confirmation-title"
aria-describedby="app-confirmation-message">
<div class="app-confirmation__accent" aria-hidden="true"></div>
<div class="app-confirmation__content">
<div class="@IconCss" aria-hidden="true">!</div>
<div class="app-confirmation__copy">
<div class="app-confirmation__eyebrow">@Eyebrow</div>
<h2 id="app-confirmation-title" class="app-confirmation__title">
@Dialog.Title
</h2>
<p id="app-confirmation-message" class="app-confirmation__message">
@Dialog.Message
</p>
</div>
</div>
<div class="app-confirmation__actions">
<button type="button"
class="btn btn-outline-secondary app-confirmation__button"
@ref="_cancelButton"
@onclick="Dialog.Cancel">
@Dialog.CancelText
</button>
<button type="button"
class="@ConfirmButtonCss"
@onclick="Dialog.Confirm">
@Dialog.ConfirmText
</button>
</div>
</section>
</div>
}
@code {
private ElementReference _cancelButton;
private bool _focusPending;
private string DialogCss =>
$"app-confirmation app-confirmation--{ToneName}";
private string IconCss =>
$"app-confirmation__icon app-confirmation__icon--{ToneName}";
private string ConfirmButtonCss =>
Dialog.Tone == UiDialogTone.Danger
? "btn app-confirmation__button app-confirmation__button--danger"
: "btn app-confirmation__button app-confirmation__button--primary";
private string ToneName => Dialog.Tone switch
{
UiDialogTone.Danger => "danger",
UiDialogTone.Information => "information",
_ => "warning"
};
private string Eyebrow => Dialog.Tone switch
{
UiDialogTone.Danger => "Acción irreversible",
UiDialogTone.Information => "Confirmación",
_ => "Revisa antes de continuar"
};
protected override void OnInitialized()
{
Dialog.Changed += HandleDialogChanged;
}
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (!_focusPending || !Dialog.IsVisible)
{
return;
}
_focusPending = false;
await _cancelButton.FocusAsync();
}
public void Dispose()
{
Dialog.Changed -= HandleDialogChanged;
}
private void HandleDialogChanged()
{
_focusPending = Dialog.IsVisible;
_ = InvokeAsync(StateHasChanged);
}
private void HandleKeyDown(KeyboardEventArgs args)
{
if (string.Equals(args.Key, "Escape", StringComparison.Ordinal))
{
Dialog.Cancel();
}
}
}

View File

@@ -0,0 +1,163 @@
.app-confirmation-backdrop {
position: fixed;
inset: 0;
z-index: 5100;
display: grid;
place-items: center;
padding: 1.25rem;
background: rgba(6, 22, 41, 0.64);
backdrop-filter: blur(5px);
overscroll-behavior: contain;
}
.app-confirmation {
position: relative;
overflow: hidden;
width: min(560px, 100%);
border: 1px solid rgba(255, 255, 255, 0.65);
border-radius: 8px;
background: #fff;
box-shadow: 0 28px 80px rgba(5, 27, 54, 0.34);
color: #12395f;
}
.app-confirmation__accent {
height: 0.35rem;
background: #d49620;
}
.app-confirmation--danger .app-confirmation__accent {
background: #c93f4f;
}
.app-confirmation--information .app-confirmation__accent {
background: #2a5caa;
}
.app-confirmation__content {
display: grid;
grid-template-columns: 3.25rem minmax(0, 1fr);
gap: 1rem;
padding: 1.5rem 1.5rem 1.1rem;
}
.app-confirmation__icon {
display: grid;
place-items: center;
width: 3.25rem;
height: 3.25rem;
border: 1px solid #efd8a8;
border-radius: 50%;
background: #fff6df;
color: #8d5e08;
font-size: 1.45rem;
font-weight: 800;
}
.app-confirmation__icon--danger {
border-color: #efc2c8;
background: #fff0f2;
color: #a82d3b;
}
.app-confirmation__icon--information {
border-color: #c4d6ef;
background: #eef5ff;
color: #214f91;
}
.app-confirmation__copy {
min-width: 0;
}
.app-confirmation__eyebrow {
margin-bottom: 0.3rem;
color: #6a7786;
font-size: 0.74rem;
font-weight: 800;
letter-spacing: 0;
text-transform: uppercase;
}
.app-confirmation__title {
margin: 0;
color: #0a315c;
font-size: 1.35rem;
font-weight: 800;
line-height: 1.25;
}
.app-confirmation__message {
margin: 0.65rem 0 0;
color: #405f7d;
line-height: 1.55;
white-space: pre-line;
}
.app-confirmation__actions {
display: flex;
justify-content: flex-end;
gap: 0.65rem;
padding: 1rem 1.5rem 1.35rem;
border-top: 1px solid #e4ebf2;
background: #f8fafc;
}
.app-confirmation__button {
min-width: 8.5rem;
min-height: 2.7rem;
border-radius: 6px;
}
.app-confirmation__button--primary {
border-color: #24539a;
background: #24539a;
color: #fff;
}
.app-confirmation__button--primary:hover,
.app-confirmation__button--primary:focus-visible {
border-color: #193f79;
background: #193f79;
color: #fff;
}
.app-confirmation__button--danger {
border-color: #b93343;
background: #b93343;
color: #fff;
}
.app-confirmation__button--danger:hover,
.app-confirmation__button--danger:focus-visible {
border-color: #922936;
background: #922936;
color: #fff;
}
@media (max-width: 575.98px) {
.app-confirmation-backdrop {
align-items: end;
padding: 0.75rem;
}
.app-confirmation__content {
grid-template-columns: 2.75rem minmax(0, 1fr);
gap: 0.8rem;
padding: 1.2rem 1rem 1rem;
}
.app-confirmation__icon {
width: 2.75rem;
height: 2.75rem;
}
.app-confirmation__actions {
flex-direction: column-reverse;
padding: 0.9rem 1rem 1rem;
}
.app-confirmation__button {
width: 100%;
}
}

View File

@@ -52,6 +52,7 @@
</div> </div>
<BusyOverlay /> <BusyOverlay />
<AppConfirmationDialog />
<div id="blazor-error-ui"> <div id="blazor-error-ui">
An unhandled error has occurred. An unhandled error has occurred.

View File

@@ -16,7 +16,7 @@
@inject IDenunciaStore DenunciaStore @inject IDenunciaStore DenunciaStore
@inject ApiDenunciasClient ApiDenuncias @inject ApiDenunciasClient ApiDenuncias
@inject UiBusyService Busy @inject UiBusyService Busy
@inject IJSRuntime JSRuntime @inject UiDialogService Dialogs
<PageTitle>Actualizaciones</PageTitle> <PageTitle>Actualizaciones</PageTitle>
@@ -720,10 +720,11 @@ else
{ {
if (d.RequiresOwnerConfirmation) if (d.RequiresOwnerConfirmation)
{ {
var confirmed = await JSRuntime.InvokeAsync<bool>( var confirmed = await Dialogs.ConfirmAsync(
"confirm", "Denuncia asignada a otro usuario",
$"La denuncia #{d.Id_Denuncia} es propiedad de {d.OwnerUsername}. " + $"La denuncia #{d.Id_Denuncia} está asignada a {d.OwnerUsername}. " +
"Puedes actualizarla porque pertenece a un usuario de tu grupo. Deseas continuar?"); "Puedes actualizarla porque pertenece a un usuario de tu grupo. La acción quedará registrada con tu usuario.",
"Configurar actualización");
if (!confirmed) if (!confirmed)
{ {
return; return;
@@ -1401,10 +1402,11 @@ else
var configuredGroups = currentGroups.GroupCodes.Count == 0 var configuredGroups = currentGroups.GroupCodes.Count == 0
? "ningún grupo" ? "ningún grupo"
: string.Join(", ", currentGroups.GroupCodes); : string.Join(", ", currentGroups.GroupCodes);
return await JSRuntime.InvokeAsync<bool>( return await Dialogs.ConfirmAsync(
"confirm", "Cambio de asignación en Gestiona",
$"El grupo de destino {selectedGroup} no pertenece a tus grupos configurados ({configuredGroups}). " + $"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?"); "Si continúas, la denuncia cambiará su asignación en Gestiona a un grupo distinto de los tuyos.",
"Cambiar asignación");
} }
catch (Exception ex) catch (Exception ex)
{ {

View File

@@ -9,6 +9,7 @@
@inject ApiDenunciasClient ApiDenuncias @inject ApiDenunciasClient ApiDenuncias
@inject IJSRuntime JSRuntime @inject IJSRuntime JSRuntime
@inject UiBusyService Busy @inject UiBusyService Busy
@inject UiDialogService Dialogs
<PageTitle>Entrada de denuncias</PageTitle> <PageTitle>Entrada de denuncias</PageTitle>
@@ -732,9 +733,10 @@
Environment.NewLine, Environment.NewLine,
reportsFromOtherOwners.Select(report => reportsFromOtherOwners.Select(report =>
$"Denuncia #{report.Progressive ?? 0}: propiedad de {report.OwnerUsername}.")); $"Denuncia #{report.Progressive ?? 0}: propiedad de {report.OwnerUsername}."));
var confirmed = await JSRuntime.InvokeAsync<bool>( var confirmed = await Dialogs.ConfirmAsync(
"confirm", "Denuncias asignadas a otros usuarios",
$"Vas a importar denuncias propiedad de otros usuarios de tu grupo:{Environment.NewLine}{Environment.NewLine}{ownerSummary}{Environment.NewLine}{Environment.NewLine}¿Deseas continuar?"); $"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) if (!confirmed)
{ {
return; return;

View File

@@ -67,15 +67,15 @@
<div class="col-12 col-xl-6"> <div class="col-12 col-xl-6">
<div class="card h-100"> <div class="card h-100">
<div class="card-body"> <div class="card-body">
<h2 class="h5">Modal de subida a Gestiona</h2> <h2 class="h5">Ventana de configuración de la subida</h2>
<p> <p>
Antes de confirmar la subida, revisa estos puntos: Al configurar un expediente nuevo o una actualización, revisa estos puntos antes de confirmar:
</p> </p>
<ul class="mb-0"> <ul class="mb-0">
<li><strong>Asunto</strong>: texto que identificara el expediente/documentos en Gestiona.</li> <li><strong>Asunto</strong>: texto que identifica el expediente en Gestiona. En las actualizaciones se muestra en modo de solo lectura.</li>
<li><strong>Grupo destino</strong>: unidad a la que se asignara el expediente.</li> <li><strong>Grupo destino</strong>: grupo al que quedará asignado el expediente en Gestiona.</li>
<li><strong>Modo de subida</strong>: puedes unir adjuntos en un PDF o subirlos de forma independiente.</li> <li><strong>Modo de subida</strong>: puedes unir adjuntos en un PDF o subirlos de forma independiente.</li>
<li><strong>Tercero</strong>: la app lo rellena desde la denuncia. Si es anonima, se usa el tercero anonimo configurado.</li> <li><strong>Tercero</strong>: la aplicación lo completa con los datos de la denuncia. Si es anónima, se utiliza el tercero anónimo configurado.</li>
<li><strong>Expedientes del tercero</strong>: puedes consultarlos antes de confirmar si necesitas contexto.</li> <li><strong>Expedientes del tercero</strong>: puedes consultarlos antes de confirmar si necesitas contexto.</li>
</ul> </ul>
</div> </div>

View File

@@ -19,7 +19,7 @@
@inject IDenunciaStore DenunciaStore @inject IDenunciaStore DenunciaStore
@inject ApiDenunciasClient ApiDenuncias @inject ApiDenunciasClient ApiDenuncias
@inject UiBusyService Busy @inject UiBusyService Busy
@inject IJSRuntime JSRuntime @inject UiDialogService Dialogs
<PageTitle>Denuncias Pendientes</PageTitle> <PageTitle>Denuncias Pendientes</PageTitle>
@@ -1336,10 +1336,15 @@ else
return true; return true;
} }
return await JSRuntime.InvokeAsync<bool>( var confirmText = string.Equals(action, "rechazar", StringComparison.OrdinalIgnoreCase)
"confirm", ? "Continuar con el rechazo"
$"La denuncia #{denuncia.Id_Denuncia} es propiedad de {denuncia.OwnerUsername}. " + : "Continuar con la apertura";
$"Puedes {action}la porque pertenece a un usuario de tu grupo. Deseas continuar?");
return await Dialogs.ConfirmAsync(
"Denuncia asignada a otro usuario",
$"La denuncia #{denuncia.Id_Denuncia} está asignada a {denuncia.OwnerUsername}. " +
$"Puedes {action}la porque pertenece a un usuario de tu grupo. La acción quedará registrada con tu usuario.",
confirmText);
} }
private async Task<bool> ConfirmSelectedGroupAsync() private async Task<bool> ConfirmSelectedGroupAsync()
@@ -1355,10 +1360,11 @@ else
var configuredGroups = currentGroups.GroupCodes.Count == 0 var configuredGroups = currentGroups.GroupCodes.Count == 0
? "ningún grupo" ? "ningún grupo"
: string.Join(", ", currentGroups.GroupCodes); : string.Join(", ", currentGroups.GroupCodes);
return await JSRuntime.InvokeAsync<bool>( return await Dialogs.ConfirmAsync(
"confirm", "Cambio de asignación en Gestiona",
$"El grupo de destino {selectedGroup} no pertenece a tus grupos configurados ({configuredGroups}). " + $"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?"); "Si continúas, la denuncia quedará asignada en Gestiona a un grupo distinto de los tuyos.",
"Cambiar asignación");
} }
catch (Exception ex) catch (Exception ex)
{ {

View File

@@ -52,6 +52,7 @@ builder.Services.AddScoped<UserState>();
builder.Services.AddSingleton<AppSessionLifetime>(); builder.Services.AddSingleton<AppSessionLifetime>();
builder.Services.AddSingleton<LoginRateLimiter>(); builder.Services.AddSingleton<LoginRateLimiter>();
builder.Services.AddScoped<UiBusyService>(); builder.Services.AddScoped<UiBusyService>();
builder.Services.AddScoped<UiDialogService>();
builder.Services.AddScoped<ApiDenunciasClient>(); builder.Services.AddScoped<ApiDenunciasClient>();
builder.Services.AddScoped<IDenunciaStore, ApiDenunciaStore>(); builder.Services.AddScoped<IDenunciaStore, ApiDenunciaStore>();
builder.Services.AddScoped<IInboxTrackingService, ApiInboxTrackingService>(); builder.Services.AddScoped<IInboxTrackingService, ApiInboxTrackingService>();

View File

@@ -0,0 +1,64 @@
namespace GestionaDenunciasAN.Services;
public enum UiDialogTone
{
Information,
Warning,
Danger
}
public sealed class UiDialogService
{
private TaskCompletionSource<bool>? _pendingConfirmation;
public event Action? Changed;
public bool IsVisible { get; private set; }
public string Title { get; private set; } = string.Empty;
public string Message { get; private set; } = string.Empty;
public string ConfirmText { get; private set; } = "Continuar";
public string CancelText { get; private set; } = "Cancelar";
public UiDialogTone Tone { get; private set; } = UiDialogTone.Warning;
public Task<bool> ConfirmAsync(
string title,
string message,
string confirmText = "Continuar",
string cancelText = "Cancelar",
UiDialogTone tone = UiDialogTone.Warning)
{
_pendingConfirmation?.TrySetResult(false);
Title = title.Trim();
Message = message.Trim();
ConfirmText = confirmText.Trim();
CancelText = cancelText.Trim();
Tone = tone;
IsVisible = true;
_pendingConfirmation = new TaskCompletionSource<bool>(
TaskCreationOptions.RunContinuationsAsynchronously);
Changed?.Invoke();
return _pendingConfirmation.Task;
}
public void Confirm()
{
Complete(true);
}
public void Cancel()
{
Complete(false);
}
private void Complete(bool confirmed)
{
var pendingConfirmation = _pendingConfirmation;
_pendingConfirmation = null;
IsVisible = false;
Changed?.Invoke();
pendingConfirmation?.TrySetResult(confirmed);
}
}