feat(ui): refresh backoffice user experience #79

Merged
leandro merged 1 commits from feature/web/078-backoffice-ui-refresh into master 2026-07-21 12:50:38 +00:00
19 changed files with 1008 additions and 895 deletions
Showing only changes of commit e8d519af9d - Show all commits

View File

@ -4,15 +4,21 @@
<Found Context="routeData">
<AuthorizeRouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)">
<NotAuthorized>
Disculpe, usted no esta autorizado a ver esta seccion.
<div class="empty-state" role="alert">
<strong>Acceso restringido</strong>
<p class="mb-0">No tenés permisos para ver esta sección.</p>
</div>
</NotAuthorized>
</AuthorizeRouteView>
<FocusOnNavigate RouteData="@routeData" Selector="h1" />
</Found>
<NotFound>
<PageTitle>Not found</PageTitle>
<PageTitle>Página no encontrada | PhronCare</PageTitle>
<LayoutView Layout="@typeof(MainLayout)">
<p role="alert">Disculpe, no hay nada en esta dirección'.</p>
<div class="empty-state" role="alert">
<strong>Página no encontrada</strong>
<p class="mb-0">No encontramos contenido en esta dirección.</p>
</div>
</LayoutView>
</NotFound>
</Router>

View File

@ -5,26 +5,24 @@
<div class="page @GetMode()">
<AuthorizeView>
<Authorized>
<div class="sidebar" style="@MinimizeMenuCss">
<aside class="sidebar @MinimizeMenuClass">
<NavMenu MinStatus="ToggleMinNavMenu"/>
</div>
</aside>
</Authorized>
</AuthorizeView>
<main>
<AuthorizeView>
<Authorized>
<div class="container-fluid" style="background-color: rgba(39, 54, 97, 0.5);">
<div class="row">
<div class="top-row px-1">
<div class="col-md-4" style="zoom:50%">
<PhToggle ToggleStateChanged="OnChangeToggleSwitchState" ToggleState=@state >
<i class="fas fa-moon fa-2xl"/>
</PhToggle>
</div>
<div class="col-md-8" style="text-align: right;">
<PhLoginState/>
</div>
</div>
<div class="topbar">
<div class="topbar-context">
<span class="topbar-kicker">PhronCare</span>
<strong>Backoffice</strong>
</div>
<div class="topbar-actions">
<PhToggle ToggleStateChanged="OnChangeToggleSwitchState" ToggleState="@state" Caption="Tema oscuro">
<i class="fas fa-moon" aria-hidden="true"></i>
</PhToggle>
<PhLoginState/>
</div>
</div>
</Authorized>
@ -43,7 +41,7 @@
ErrorIcon="fa fa-ban" />
@code {
private bool state=false;
private string MinimizeMenuCss => navMenuService.Minimized ? "width:82px" : string.Empty;
private string MinimizeMenuClass => navMenuService.Minimized ? "sidebar-minimized" : string.Empty;
protected void ToggleMinNavMenu(bool status)
{
navMenuService.Minimized = status;

View File

@ -1,84 +1,16 @@
.page {
position: relative;
display: flex;
flex-direction: column;
}
main {
flex: 1;
}
.sidebar {
background-color: rgb(39, 54, 97);
/*background-image: linear-gradient(180deg, rgb(39, 54, 97) 70%, rgb(39, 54, 97) 99%);*/
}
.top-row {
/* background-color: rgb(221, 226, 234);*/
background-color: var(--fondo-resaltado);
border-bottom: 1px solid #d6d5d5;
justify-content: flex-end;
height: 3.5rem;
display: flex;
align-items: center;
}
.top-row ::deep a, .top-row ::deep .btn-link {
white-space: nowrap;
margin-left: 1.5rem;
text-decoration: none;
}
.top-row ::deep a:hover, .top-row ::deep .btn-link:hover {
text-decoration: underline;
}
.top-row ::deep a:first-child {
overflow: hidden;
text-overflow: ellipsis;
}
@media (max-width: 640.98px) {
.top-row:not(.auth) {
display: none;
}
.top-row.auth {
justify-content: space-between;
}
.top-row ::deep a, .top-row ::deep .btn-link {
margin-left: 0;
}
}
@media (min-width: 641px) {
.page {
flex-direction: row;
}
.sidebar {
width: 225px;
height: 100vh;
position: sticky;
top: 0;
}
.top-row {
position: sticky;
top: 0;
z-index: 1;
}
.top-row.auth ::deep a:first-child {
flex: 1;
text-align: right;
width: 0;
}
.top-row, article {
padding-left: 2rem !important;
padding-right: 1.5rem !important;
}
.page { position: relative; display: flex; min-height: 100vh; background: var(--ph-canvas); }
main { flex: 1; min-width: 0; }
.sidebar { position: sticky; top: 0; z-index: 20; width: 200px; height: 100vh; flex: 0 0 200px; overflow: hidden; background: var(--ph-sidebar); box-shadow: 4px 0 18px rgba(9, 18, 38, .08); transition: width .2s ease, flex-basis .2s ease; }
.sidebar-minimized { width: 57px; flex-basis: 57px; }
.topbar { position: sticky; top: 0; z-index: 15; display: flex; min-height: 4.25rem; align-items: center; justify-content: space-between; gap: 1rem; padding: .65rem 1.5rem; background: color-mix(in srgb, var(--ph-surface) 92%, transparent); border-bottom: 1px solid var(--ph-border); box-shadow: var(--ph-shadow-sm); backdrop-filter: blur(12px); }
.topbar-context { display: flex; flex-direction: column; min-width: 0; line-height: 1.15; }
.topbar-context strong { color: var(--ph-text); font-size: .98rem; }
.topbar-kicker { color: var(--ph-text-muted); font-size: .7rem; font-weight: 700; letter-spacing: .08em; text-transform: uppercase; }
.topbar-actions { display: flex; align-items: center; justify-content: flex-end; gap: 1rem; min-width: 0; }
@media (max-width: 767.98px) {
.page { display: block; }
.sidebar, .sidebar-minimized { position: relative; width: 100%; height: auto; min-height: 4rem; }
.topbar { position: relative; min-height: 3.75rem; padding: .5rem .85rem; }
.topbar-context { display: none; }
.topbar-actions { width: 100%; justify-content: space-between; gap: .5rem; }
}

View File

@ -1,27 +1,31 @@
@inject NavMenuService navMenuService
<div class="top-row ps-3 navbar navbar-dark border-bottom" style="padding: 0; border:0px solid red;">
<div class="container-fluid" style="padding:0; border:0px solid green;">
<a class="navbar-brand d-flex align-items-center" href="" style="height: 56px; border:0px solid yellow; display: block;">
<div class="sidebar-header navbar navbar-dark @(navMenuService.Minimized ? "brand-minimized" : string.Empty)">
<div class="container-fluid">
<a class="navbar-brand" href="" aria-label="Ir al inicio de PhronCare">
<img src="images/SaludLAB.png" alt="" />
@if (!navMenuService.Minimized)
{
<img src="images/Icono.png" alt="Logo" style="height: 50px; display: block; border:0px solid blue;" />
<span class="brand-copy">
<span class="brand-name"><strong>Phron</strong>Care</span>
<small>by SaludLAB</small>
</span>
}
</a>
<button title="Navigation menu" class="navbar-toggler" @onclick="ToggleNavMenu">
<button type="button" title="Abrir menú de navegación" aria-label="Abrir menú de navegación" class="navbar-toggler" @onclick="ToggleNavMenu">
<span class="navbar-toggler-icon"></span>
</button>
</div>
</div>
<div class="@($"{NavMenuCssClass} nav-scrollable bg-dark text-white")" @onclick="ToggleNavMenu" style="padding-top:4px;">
<nav class="flex-column text-center">
<div class="@($"{NavMenuCssClass} nav-scrollable {(navMenuService.Minimized ? "nav-minimized" : string.Empty)}")" @onclick="ToggleNavMenu">
<nav class="flex-column" aria-label="Navegación principal">
<div class="nav-item px-2 py-0">
<NavLink class="nav-link text-white d-flex align-items-center gap-2 py-0 px-2" href="/" Match="NavLinkMatch.All" activeClass="bg-secondary text-white">
<span class="oi oi-home" aria-hidden="true"></span>
@if (!navMenuService.Minimized)
{
<label class="mb-0">Home</label>
<label class="mb-0">Inicio</label>
}
</NavLink>
</div>
@ -47,7 +51,7 @@
</div>
</AuthorizeView>
<div class="nav-item px-2 py-0">
<NavLink class="nav-link text-white d-flex align-items-center gap-2 py-0 px-2" @onclick="() => expStock = !expStock">
<NavLink class="nav-link text-white d-flex align-items-center gap-2 py-0 px-2" @onclick="ToggleStockMenu">
<span class="oi oi-box" aria-hidden="true"></span>
@if (!navMenuService.Minimized)
{
@ -55,7 +59,7 @@
}
</NavLink>
@if (expStock)
@if (expStock && !navMenuService.Minimized)
{
<ul class="nav flex-column">
<!-- Expediciones -->
@ -108,7 +112,7 @@
</div>
<div class="nav-item px-2 py-0">
<NavLink class="nav-link text-white d-flex align-items-center gap-2 py-0 px-2" @onclick="() => expClientes = !expClientes">
<NavLink class="nav-link text-white d-flex align-items-center gap-2 py-0 px-2" @onclick="ToggleSalesMenu">
<span class="oi oi-briefcase" aria-hidden="true"></span>
@if (!navMenuService.Minimized)
{
@ -116,7 +120,7 @@
}
</NavLink>
@if (expClientes)
@if (expClientes && !navMenuService.Minimized)
{
<ul class="nav flex-column">
<div class="nav-item ps-4 py-0 border-start border-2 border-white">
@ -178,13 +182,34 @@
</NavLink>
</div>
</nav>
<div class="sidebar-footer">
@if (!navMenuService.Minimized)
{
<div class="client-context" title="Cliente de esta instancia">
<img src="images/Icono.png" alt="" />
<span><small>Entorno</small><strong>BIODEC</strong></span>
</div>
}
<button type="button"
class="collapse-control"
title="@(navMenuService.Minimized ? "Expandir menú" : "Contraer menú")"
aria-label="@(navMenuService.Minimized ? "Expandir menú lateral" : "Contraer menú lateral")"
aria-pressed="@navMenuService.Minimized"
@onclick:stopPropagation
@onclick="ToggleIconMenu">
<i class="fas @(navMenuService.Minimized ? "fa-eye-slash" : "fa-eye")" aria-hidden="true"></i>
@if (!navMenuService.Minimized)
{
<span>Contraer menú</span>
}
</button>
</div>
</div>
@code {
private bool collapseNavMenu = true;
private bool minimizeNavMenu = false;
private bool expStock = true;
private bool expClientes = true;
private bool expStock = false;
private bool expClientes = false;
private string? NavMenuCssClass => collapseNavMenu ? "collapse" : null;
@ -199,9 +224,38 @@
collapseNavMenu = !collapseNavMenu;
}
private async void ToggleIconMenu()
private async Task ToggleStockMenu()
{
minimizeNavMenu = !minimizeNavMenu;
await MinStatus.InvokeAsync(minimizeNavMenu);
if (navMenuService.Minimized)
{
await MinStatus.InvokeAsync(false);
expStock = true;
return;
}
expStock = !expStock;
}
private async Task ToggleSalesMenu()
{
if (navMenuService.Minimized)
{
await MinStatus.InvokeAsync(false);
expClientes = true;
return;
}
expClientes = !expClientes;
}
private async Task ToggleIconMenu()
{
var minimized = !navMenuService.Minimized;
if (minimized)
{
expStock = false;
expClientes = false;
}
await MinStatus.InvokeAsync(minimized);
}
}

View File

@ -1,83 +1,41 @@
.navbar-toggler {
background-color: rgba(255, 255, 255, 0.1);
}
.icon-white {
color: rgb(255, 255, 255);
}
.top-row {
height: 3.5rem;
background-color: rgba(0,0,0,0.4);
}
.navbar-brand {
font-size: 1.1rem;
}
.oi {
width: 2rem;
font-size: 1.1rem;
vertical-align: text-top;
top: -2px;
}
.nav-item {
font-size: 0.9rem;
padding-bottom: 0.5rem;
}
.nav-item:first-of-type {
padding-top: 1rem;
}
.nav-item:last-of-type {
padding-bottom: 1rem;
}
.nav-item ::deep a {
color: #d7d7d7;
border-radius: 4px;
height: 3rem;
display: flex;
align-items: center;
line-height: 3rem;
}
.nav-item ::deep a.active {
background-color: rgba(255,255,255,0.25);
color: white;
}
.nav-item ::deep a:hover {
background-color: rgba(255,255,255,0.1);
color: white;
}
@media (max-width: 992px) {
.nav-link .text-hide-on-small {
display: none;
}
}
/* Muestra el texto dentro de .nav-link en pantallas medianas o grandes ('lg' o más grande) */
@media (min-width: 993px) {
.nav-link .text-hide-on-small {
display: inline;
}
}
@media (min-width: 641px) {
.navbar-toggler {
display: none;
}
.collapse {
/* Never collapse the sidebar for wide screens */
display: block;
}
.nav-scrollable {
/* Allow sidebar to scroll for tall menus */
height: calc(100vh - 3.5rem);
overflow-y: auto;
}
}
.sidebar-header { min-height: 4.25rem; padding: .55rem .75rem; background: var(--ph-sidebar-deep); border-bottom: 1px solid rgba(255, 255, 255, .08); }
.sidebar-header .container-fluid { flex-wrap: nowrap; padding: 0; }
.sidebar-header.brand-minimized { padding-inline: .5rem; }
.sidebar-header.brand-minimized .container-fluid { justify-content: center; }
.navbar-brand { display: flex; min-width: 0; align-items: center; gap: .65rem; margin: 0; color: #fff; font-size: 1.08rem; letter-spacing: -.02em; }
.navbar-brand img { display: block; width: 40px; height: 40px; flex: 0 0 40px; object-fit: contain; border-radius: .65rem; }
.navbar-brand .brand-copy { display: flex; flex-direction: column; line-height: 1; white-space: nowrap; }
.navbar-brand .brand-name { display: inline-block; white-space: nowrap; }
.navbar-brand small { margin-top: .28rem; color: var(--ph-sidebar-muted); font-size: .58rem; font-weight: 700; letter-spacing: .13em; text-transform: uppercase; }
.navbar-toggler { flex: 0 0 auto; padding: .35rem .5rem; background: rgba(255, 255, 255, .08); border: 1px solid rgba(255, 255, 255, .14); border-radius: .5rem; }
.nav-scrollable { display: flex; height: calc(100vh - 4.25rem); flex-direction: column; padding: .75rem .5rem .65rem; overflow: hidden; background: var(--ph-sidebar); }
.nav-scrollable > nav { min-height: 0; flex: 1 1 auto; overflow-x: hidden; overflow-y: auto; -webkit-user-select: none; user-select: none; }
.nav-item { padding: .12rem 0 !important; font-size: .875rem; }
.nav-item ::deep a { display: flex; min-height: 2.65rem; align-items: center; gap: .65rem; padding: .55rem .72rem !important; color: var(--ph-sidebar-text) !important; line-height: 1.25; border-radius: .65rem; text-decoration: none; transition: color .15s ease, background-color .15s ease; }
.nav-item ::deep a label { margin: 0; color: inherit; font-size: inherit; font-weight: 550; cursor: pointer; }
.nav-item ::deep a .oi, .nav-item ::deep a > i { width: 1.35rem; flex: 0 0 1.35rem; color: var(--ph-sidebar-muted); font-size: 1rem; text-align: center; }
.nav-item ::deep a:hover { color: #fff !important; background: rgba(255, 255, 255, .075); }
.nav-item ::deep a:hover .oi, .nav-item ::deep a:hover > i { color: #fff; }
.nav-item ::deep a.active { color: #fff !important; background: rgba(126, 162, 235, .2) !important; box-shadow: inset 3px 0 0 #8daef2; }
.nav-item ::deep a.active .oi, .nav-item ::deep a.active > i { color: #a9c2f5; }
.nav-scrollable.nav-minimized { padding-inline: .5rem; }
.nav-scrollable.nav-minimized .nav-item ::deep a { justify-content: center; padding-inline: 0 !important; }
.nav-scrollable.nav-minimized .nav-item ::deep a .oi, .nav-scrollable.nav-minimized .nav-item ::deep a > i { margin: 0 !important; }
.nav-item ul { gap: .05rem; margin: .2rem 0 .4rem; padding: 0; border: 0; }
.nav-item ul .nav-item { padding-left: 0 !important; border: 0 !important; }
.nav-item ul .nav-item ::deep a { min-height: 2.25rem; padding: .4rem .72rem !important; color: var(--ph-sidebar-muted) !important; font-size: .78rem; font-weight: 500; }
.nav-item ul .nav-item ::deep a .oi, .nav-item ul .nav-item ::deep a > i { color: color-mix(in srgb, var(--ph-sidebar-muted) 78%, transparent); font-size: .86rem; }
.nav-item ul .nav-item ::deep a:hover { color: var(--ph-sidebar-text) !important; background: rgba(255, 255, 255, .055); }
.nav-item ul .nav-item ::deep a.active { color: #fff !important; background: rgba(126, 162, 235, .13) !important; box-shadow: inset 2px 0 0 rgba(141, 174, 242, .75); }
.sidebar-footer { flex: 0 0 auto; padding: .7rem .25rem 0; border-top: 1px solid rgba(255, 255, 255, .1); -webkit-user-select: none; user-select: none; }
.client-context { display: flex; align-items: center; gap: .6rem; margin: 0 .35rem .65rem; padding: .45rem .5rem; color: var(--ph-sidebar-text); background: rgba(255, 255, 255, .04); border-radius: .55rem; }
.client-context img { width: 2.25rem; height: 1.25rem; object-fit: contain; }
.client-context span { display: flex; flex-direction: column; line-height: 1.05; }
.client-context small { color: var(--ph-sidebar-muted); font-size: .58rem; font-weight: 700; letter-spacing: .08em; text-transform: uppercase; }
.client-context strong { margin-top: .2rem; color: var(--ph-sidebar-text); font-size: .72rem; letter-spacing: .04em; }
.collapse-control { display: flex; width: 100%; min-height: 2.5rem; align-items: center; justify-content: center; gap: .6rem; padding: .5rem .65rem; color: var(--ph-sidebar-muted); background: transparent; border: 1px solid transparent; border-radius: .6rem; font-size: .78rem; font-weight: 650; transition: color .15s ease, background-color .15s ease, border-color .15s ease; }
.collapse-control i { width: 1.2rem; font-size: .9rem; }
.collapse-control:hover { color: #fff; background: rgba(255, 255, 255, .075); border-color: rgba(255, 255, 255, .08); }
.collapse-control:focus-visible { outline: 3px solid rgba(141, 174, 242, .35); }
@media (min-width: 768px) { .navbar-toggler { display: none; } .nav-scrollable.collapse { display: flex; } }
@media (max-width: 767.98px) { .sidebar-header { min-height: 4rem; } .nav-scrollable { height: auto; max-height: calc(100vh - 4rem); } .nav-scrollable > nav { overflow-y: auto; } .sidebar-footer { display: none; } .collapse { display: none; } }

View File

@ -12,75 +12,121 @@
@inject ILoginService _iLoginService
@inject IToastService toastService
<div class="row" style="margin-top: 30px;">
<div class="col-md-3 col-sm-12" style="background-color: rgb(39, 54, 97); border-radius: 10px; color: white; padding: 20px; margin: 0 auto auto auto;">
<img src="images/Logo.png" class="img-fluid" />
<PageTitle>Ingresar | PhronCare · SaludLAB</PageTitle>
<section class="login-shell">
<div class="login-showcase" aria-hidden="true">
<div class="showcase-brand">
<img src="images/SaludLAB.png" alt="" />
<span class="brand-copy">
<span class="brand-name"><strong>Phron</strong>Care</span>
<small>by SaludLAB</small>
</span>
</div>
<div class="showcase-copy">
<span class="showcase-eyebrow">Gestión de salud</span>
<h1>Una operación más clara, conectada y eficiente.</h1>
<p>Centralizá la gestión asistencial, comercial y logística en un único espacio seguro.</p>
</div>
<div class="showcase-footer"><i class="fas fa-shield-halved"></i> Acceso seguro al Backoffice</div>
</div>
<div class="login-panel">
<div class="login-card">
<div class="mobile-brand">
<img src="images/SaludLAB.png" alt="Logo SaludLAB" />
<span class="brand-copy">
<span class="brand-name"><strong>Phron</strong>Care</span>
<small>by SaludLAB</small>
</span>
</div>
@if (!isSecret)
{
<h3>Iniciar sesión</h3>
<EditForm Model="login2FAModel" OnInvalidSubmit="LoginAsync">
{
<div class="login-heading">
<span class="login-eyebrow">Bienvenido</span>
<h2>Iniciar sesión</h2>
<p>Ingresá tus credenciales para continuar.</p>
</div>
<EditForm Model="login2FAModel" OnSubmit="LoginAsync">
<div class="form-group">
<label>Usuario</label>
<div>
<InputText Class="form-control" @bind-Value=@login2FAModel.UserName />
<label for="username">Usuario</label>
<div class="input-with-icon">
<i class="fas fa-user" aria-hidden="true"></i>
<InputText id="username" autocomplete="username" placeholder="Ingresá tu usuario" Class="form-control" @bind-Value="@login2FAModel.UserName" />
</div>
</div>
<div class="form-group">
<label>Contraseña</label>
<div>
<InputText type="password" Class="form-control" @bind-Value=login2FAModel.Password />
<label for="password">Contraseña</label>
<div class="input-with-icon">
<i class="fas fa-lock" aria-hidden="true"></i>
<InputText id="password" type="password" autocomplete="current-password" placeholder="Ingresá tu contraseña" Class="form-control" @bind-Value="login2FAModel.Password" />
</div>
</div>
@if (codeTwoFactor)
{
<div class="form-group">
<label for="code">Código</label>
<InputText type="text" id="code" class="form-control" @bind-Value=login2FAModel.Code />
<label for="code">Código de verificación</label>
<div class="input-with-icon">
<i class="fas fa-shield-halved" aria-hidden="true"></i>
<InputText type="text" id="code" inputmode="numeric" autocomplete="one-time-code" placeholder="Código de seguridad" class="form-control" @bind-Value="login2FAModel.Code" />
</div>
<small class="field-hint">Ingresá el código de autenticación de dos factores.</small>
</div>
}
<br />
<div class="col-12" style="text-align:center;">
<button type="submit" class="btn btn-primary" @onclick="LoginAsync">
<span class="fa fa-key"></span> Entrar</button>
<button class="btn btn-warning btn-circle" style="float:right;" @onclick="ToggleIsSecret">
<span class="fa fa-user-secret"></span>
</button>
<button type="submit" class="btn btn-primary login-submit" disabled="@isSubmitting">
@if (isSubmitting) { <span class="spinner-border spinner-border-sm" aria-hidden="true"></span> }
else { <span class="fa fa-arrow-right-to-bracket" aria-hidden="true"></span> }
@(isSubmitting ? "Ingresando…" : "Ingresar")
</button>
<div class="login-help">
<span>¿Problemas para ingresar?</span>
<button type="button" class="reset-link" @onclick="ToggleIsSecret">Restablecer contraseña</button>
</div>
</EditForm>
}
else
{
<h3>Restablecer password</h3>
<div class="login-heading">
<span class="login-eyebrow">Recuperación de acceso</span>
<h2>Restablecer contraseña</h2>
<p>Completá los datos y el token recibido por correo.</p>
</div>
<EditForm Model="resetPassword" OnValidSubmit="ResetPasswordAsync">
<DataAnnotationsValidator />
<div class="form-group">
<label>Email</label>
<InputText @bind-Value="resetPassword.Email" class="form-control" />
<label for="reset-email">Email</label>
<InputText id="reset-email" type="email" autocomplete="email" @bind-Value="resetPassword.Email" class="form-control" />
<ValidationMessage For="@(() => resetPassword.Email)" />
</div>
<div class="form-group">
<label>Nueva Contraseña</label>
<InputText @bind-Value="resetPassword.Password" type="password" class="form-control" />
<label for="new-password">Nueva contraseña</label>
<InputText id="new-password" @bind-Value="resetPassword.Password" type="password" autocomplete="new-password" class="form-control" />
<ValidationMessage For="@(() => resetPassword.Password)" />
</div>
<div class="form-group">
<label>Confirmar Contraseña</label>
<InputText @bind-Value="resetPassword.ConfirmPassword" type="password" class="form-control" />
<label for="confirm-password">Confirmar contraseña</label>
<InputText id="confirm-password" @bind-Value="resetPassword.ConfirmPassword" type="password" autocomplete="new-password" class="form-control" />
<ValidationMessage For="@(() => resetPassword.ConfirmPassword)" />
</div>
<div class="form-group">
<label>Token</label>
<InputTextArea @bind-Value="resetPassword.Token" class="form-control" />
<label for="reset-token">Token</label>
<InputTextArea id="reset-token" rows="3" @bind-Value="resetPassword.Token" class="form-control" />
<ValidationMessage For="@(() => resetPassword.Token)" />
</div>
<br/>
<div class="col-12" style="text-align:center;">
<button type="submit" class="btn btn-primary"><span class="fa fa-vault"></span> Restablecer</button>
<button class="btn btn-warning btn-circle" style="fData.Login.Loginght;" @onclick="ToggleIsSecret">
<span class="fa fa-circle-left"></span>
</button>
<button type="submit" class="btn btn-primary login-submit" disabled="@isSubmitting">
@if (isSubmitting) { <span class="spinner-border spinner-border-sm" aria-hidden="true"></span> }
else { <span class="fa fa-key" aria-hidden="true"></span> }
@(isSubmitting ? "Restableciendo…" : "Restablecer contraseña")
</button>
<div class="login-help">
<button type="button" class="reset-link" @onclick="ToggleIsSecret"><i class="fas fa-arrow-left"></i> Volver al inicio de sesión</button>
</div>
</EditForm>
}
<div class="login-legal">PhronCare by SaludLAB · Entorno BIODEC</div>
</div>
</div>
</div>
</section>
@code {
Login loginModel = new Login();
@ -88,10 +134,15 @@
ResetPassword resetPassword = new ResetPassword();
public bool isSecret = false;
bool codeTwoFactor = false;
bool isSubmitting = false;
string OTPCode = string.Empty;
private async Task LoginAsync()
{
if (isSubmitting) return;
isSubmitting = true;
try
{
if (codeTwoFactor && login2FAModel.Code.Length > 4)
{
string requestUrl = $"api/Authentication/login-2FA?code={login2FAModel.Code}&username={login2FAModel.UserName}";
@ -136,9 +187,18 @@
toastService.ShowError(tokenResponse);
}
}
}
finally
{
isSubmitting = false;
}
}
private async Task ResetPasswordAsync()
{
if (isSubmitting) return;
isSubmitting = true;
try
{
if (resetPassword.Password != resetPassword.ConfirmPassword)
{
toastService.ShowError("Las contraseñas no coinciden. ¡Inténtalo de nuevo!");
@ -151,6 +211,11 @@
{
await ResetingPassword();
}
}
finally
{
isSubmitting = false;
}
}
private async Task ResetingPassword()
{

View File

@ -1,9 +1,43 @@
.btn-circle {
width: 40px;
height: 40px;
padding: 6px 0px;
border-radius: 50px;
text-align: center;
font-size: 20px;
line-height: 1.42857;
.login-shell { display: grid; min-height: calc(100vh - 3rem); grid-template-columns: minmax(360px, .9fr) minmax(430px, 1.1fr); overflow: hidden; background: var(--ph-surface); border: 1px solid var(--ph-border); border-radius: 1.25rem; box-shadow: var(--ph-shadow-md); }
.login-showcase { position: relative; display: flex; min-height: 650px; flex-direction: column; justify-content: space-between; padding: 2.25rem; overflow: hidden; color: #fff; background: radial-gradient(circle at 85% 15%, rgba(83, 199, 184, .28), transparent 32%), linear-gradient(145deg, #263b70 0%, #1b2a50 52%, #15213e 100%); }
.login-showcase::before, .login-showcase::after { position: absolute; content: ""; border: 1px solid rgba(255,255,255,.08); border-radius: 50%; }
.login-showcase::before { right: -12rem; bottom: -12rem; width: 30rem; height: 30rem; }
.login-showcase::after { right: -5rem; bottom: -5rem; width: 18rem; height: 18rem; }
.showcase-brand { position: relative; z-index: 1; display: flex; align-items: center; gap: .75rem; font-size: 1.35rem; }
.showcase-brand img { width: 46px; height: 46px; object-fit: contain; border-radius: .75rem; }
.showcase-brand .brand-copy, .mobile-brand .brand-copy { display: flex; flex-direction: column; line-height: 1; }
.showcase-brand .brand-name, .mobile-brand .brand-name { display: inline-block; white-space: nowrap; }
.showcase-brand small, .mobile-brand small { margin-top: .3rem; color: rgba(232,239,251,.66); font-size: .58rem; font-weight: 700; letter-spacing: .11em; text-transform: uppercase; }
.showcase-copy { position: relative; z-index: 1; max-width: 32rem; }
.showcase-eyebrow, .login-eyebrow { display: block; margin-bottom: .7rem; color: #72d6ca; font-size: .72rem; font-weight: 750; letter-spacing: .11em; text-transform: uppercase; }
.showcase-copy h1 { margin: 0; color: #fff; font-size: clamp(2rem, 3.2vw, 3.15rem); font-weight: 720; line-height: 1.08; }
.showcase-copy p { max-width: 28rem; margin: 1.15rem 0 0; color: rgba(232,239,251,.72); font-size: 1rem; line-height: 1.65; }
.showcase-footer { position: relative; z-index: 1; display: flex; align-items: center; gap: .55rem; color: rgba(232,239,251,.68); font-size: .75rem; }
.login-panel { display: flex; align-items: center; justify-content: center; padding: clamp(2rem, 5vw, 5rem); background: var(--ph-surface); }
.login-card { width: 100%; max-width: 430px; }
.mobile-brand { display: none; }
.login-heading { margin-bottom: 1.75rem; }
.login-heading h2 { margin: 0; color: var(--ph-text); font-size: 1.85rem; font-weight: 750; }
.login-heading p { margin: .55rem 0 0; color: var(--ph-text-muted); font-size: .92rem; }
.login-eyebrow { color: var(--ph-primary); }
.form-group { margin-bottom: 1rem; }
.input-with-icon { position: relative; }
.input-with-icon > i { position: absolute; top: 50%; left: .95rem; z-index: 1; color: var(--ph-text-muted); font-size: .85rem; transform: translateY(-50%); pointer-events: none; }
.input-with-icon ::deep .form-control { padding-left: 2.65rem; }
.field-hint { display: block; margin-top: .4rem; color: var(--ph-text-muted); font-size: .75rem; }
.login-submit { display: flex; width: 100%; min-height: 2.85rem; align-items: center; justify-content: center; gap: .55rem; margin-top: 1.35rem; }
.login-help { display: flex; align-items: center; justify-content: center; gap: .35rem; margin-top: 1.2rem; color: var(--ph-text-muted); font-size: .78rem; text-align: center; }
.reset-link { padding: .2rem; color: var(--ph-primary); background: transparent; border: 0; font: inherit; font-weight: 650; cursor: pointer; }
.reset-link:hover { text-decoration: underline; }
.reset-link:focus-visible { outline: 3px solid var(--ph-focus); border-radius: .25rem; }
.login-legal { margin-top: 2rem; color: var(--ph-text-muted); font-size: .7rem; text-align: center; }
@media (max-width: 991.98px) { .login-shell { grid-template-columns: minmax(300px, .75fr) minmax(400px, 1.25fr); } .login-showcase { padding: 1.75rem; } }
@media (max-width: 767.98px) {
.login-shell { display: block; min-height: auto; border: 0; border-radius: 0; box-shadow: none; }
.login-showcase { display: none; }
.login-panel { min-height: calc(100vh - 2rem); align-items: flex-start; padding: 1.5rem 1rem 2rem; }
.mobile-brand { display: flex; align-items: center; gap: .65rem; margin-bottom: 2.5rem; color: var(--ph-text); font-size: 1.2rem; }
.mobile-brand img { width: 42px; height: 42px; object-fit: contain; border-radius: .65rem; }
.mobile-brand small { color: var(--ph-text-muted); }
.login-heading h2 { font-size: 1.6rem; }
}

View File

@ -2,92 +2,120 @@
@using phronCare.UIBlazor.Pages.Authorization
@inject NavigationManager Navigation
<PageTitle>PhronCare</PageTitle>
<PageTitle>PhronCare · SaludLAB</PageTitle>
<AuthorizeView>
<Authorized>
<section class="home-dashboard">
<div class="home-hero">
<section class="home-dashboard" aria-label="Panel principal">
<header class="home-intro">
<div>
<span class="home-eyebrow">BackOffice operativo</span>
<h1>Bienvenido a PhronCare</h1>
<p>
Gestion integral para empresas de internacion domiciliaria,
ortopedia y prestaciones medicas.
</p>
<span class="section-eyebrow">Centro operativo</span>
<h1>Panel principal</h1>
<p>Gestión asistencial, comercial y logística de PhronCare.</p>
</div>
<div class="home-hero-status" aria-label="Estado general">
<span class="home-status-dot"></span>
<div class="platform-status" aria-label="Estado general: plataforma operativa">
<span class="status-dot"></span>
Plataforma operativa
</div>
</div>
</header>
<div class="home-section-header">
<h2>Accesos rapidos</h2>
<span>Modulos principales</span>
</div>
<div class="quick-access-grid">
@foreach (var item in QuickAccessItems)
{
<button type="button" class="quick-access-card" @onclick="() => NavigateTo(item.Url)">
<span class="quick-access-icon @item.AccentClass">
<i class="@item.Icon" aria-hidden="true"></i>
</span>
<span class="quick-access-content">
<strong>@item.Title</strong>
<small>@item.Description</small>
</span>
<i class="fas fa-chevron-right quick-access-arrow" aria-hidden="true"></i>
</button>
}
</div>
<div class="home-content-grid">
<section>
<div class="home-section-header">
<h2>Resumen operativo</h2>
<span>Valores temporales</span>
<section class="dashboard-section" aria-labelledby="daily-title">
<div class="section-heading">
<div>
<span class="section-eyebrow">Accesos principales</span>
<h2 id="daily-title">Operación diaria</h2>
</div>
<p>Herramientas centrales para la atención y la gestión comercial.</p>
</div>
<div class="kpi-grid">
<div class="featured-grid">
@foreach (var item in PrimaryItems)
{
<button type="button" class="featured-card @item.ToneClass" @onclick="() => NavigateTo(item.Url)">
<span class="featured-card-topline">
<span class="feature-icon"><i class="@item.Icon" aria-hidden="true"></i></span>
<span class="feature-tag">@item.Category</span>
</span>
<span class="featured-copy">
<strong>@item.Title</strong>
<small>@item.Description</small>
</span>
<span class="card-action">@item.Action <i class="fas fa-arrow-right" aria-hidden="true"></i></span>
</button>
}
</div>
</section>
<section class="dashboard-section" aria-labelledby="management-title">
<div class="section-heading">
<div>
<span class="section-eyebrow">Administración</span>
<h2 id="management-title">Gestión del backoffice</h2>
</div>
<p>Módulos que acompañan la operación comercial, documental y logística.</p>
</div>
<div class="management-grid">
@foreach (var item in ManagementItems)
{
<button type="button" class="management-card" @onclick="() => NavigateTo(item.Url)">
<span class="management-icon @item.AccentClass"><i class="@item.Icon" aria-hidden="true"></i></span>
<span class="management-copy">
<strong>@item.Title</strong>
<small>@item.Description</small>
</span>
<i class="fas fa-arrow-right management-arrow" aria-hidden="true"></i>
</button>
}
</div>
</section>
<section class="dashboard-section" aria-labelledby="overview-title">
<div class="section-heading section-heading-compact">
<div>
<span class="section-eyebrow">Panorama actual</span>
<h2 id="overview-title">Resumen operativo</h2>
</div>
<span class="sample-label">Datos de muestra</span>
</div>
<div class="overview-grid">
<div class="metrics-panel">
@foreach (var item in SummaryItems)
{
<div class="kpi-card">
<div class="kpi-icon @item.AccentClass">
<i class="@item.Icon" aria-hidden="true"></i>
</div>
<div>
<article class="metric-item">
<span class="metric-icon @item.AccentClass"><i class="@item.Icon" aria-hidden="true"></i></span>
<span class="metric-copy">
<strong>@item.Value</strong>
<span>@item.Title</span>
</div>
</div>
}
</div>
</section>
<section>
<div class="home-section-header">
<h2>Actividad reciente</h2>
<span>Simulada</span>
</div>
<div class="activity-list">
@foreach (var item in RecentActivity)
{
<div class="activity-item">
<span class="activity-icon @item.AccentClass">
<i class="@item.Icon" aria-hidden="true"></i>
<small>@item.Title</small>
</span>
<div>
<strong>@item.Title</strong>
<small>@item.Description</small>
</div>
</div>
</article>
}
</div>
</section>
</div>
<div class="activity-panel">
<div class="activity-header">
<div>
<h3>Actividad reciente</h3>
<p>Últimos movimientos registrados</p>
</div>
<i class="fas fa-clock" aria-hidden="true"></i>
</div>
<div class="activity-list">
@foreach (var item in RecentActivity)
{
<div class="activity-item">
<span class="activity-icon @item.AccentClass"><i class="@item.Icon" aria-hidden="true"></i></span>
<span>
<strong>@item.Title</strong>
<small>@item.Description</small>
</span>
</div>
}
</div>
</div>
</div>
</section>
</section>
</Authorized>
<NotAuthorized>
@ -97,23 +125,27 @@
@code
{
private readonly List<QuickAccessItem> QuickAccessItems =
private readonly List<PrimaryAccessItem> PrimaryItems =
[
new("Pacientes", "Gestion de pacientes y datos asistenciales.", "/sales/patients", "fas fa-user-injured", "accent-blue"),
new("Presupuestos", "Consulta y seguimiento de presupuestos.", "/quotes/dashboard", "fas fa-file-invoice-dollar", "accent-green"),
new("Ventas", "Clientes, productos y circuito comercial.", "/sales/customers", "fas fa-briefcase", "accent-indigo"),
new("Remitos", "Emision y control de remitos.", "/deliverynotes", "fas fa-truck", "accent-orange"),
new("Sales Documents", "Documentos comerciales y facturacion.", "/salesdocuments", "fas fa-receipt", "accent-teal"),
new("Stock", "Productos medicos y trazabilidad.", "/stock/products", "fas fa-boxes-stacked", "accent-violet"),
new("Tickets", "Alta y seguimiento de tickets.", "/dashboardpanel", "fas fa-ticket-alt", "accent-red")
new("Presupuestos", "Creá, consultá y realizá el seguimiento del circuito de presupuestos.", "/quotes/dashboard", "fas fa-file-invoice-dollar", "Gestión comercial", "Ver presupuestos", "tone-green"),
new("Notas de expedición", "Emití, consultá y controlá las entregas asociadas a la operación logística.", "/deliverynotes", "fas fa-truck", "Gestión logística", "Ver notas de expedición", "tone-blue")
];
private readonly List<ManagementAccessItem> ManagementItems =
[
new("Pacientes", "Información asistencial", "/sales/patients", "fas fa-user-injured", "accent-blue"),
new("Ventas", "Clientes y productos", "/sales/customers", "fas fa-briefcase", "accent-indigo"),
new("Documentos comerciales", "Documentación y facturación", "/salesdocuments", "fas fa-receipt", "accent-teal"),
new("Stock", "Productos y trazabilidad", "/stock/products", "fas fa-boxes-stacked", "accent-violet"),
new("Tickets", "Alta y seguimiento", "/dashboardpanel", "fas fa-ticket-alt", "accent-red")
];
private readonly List<SummaryItem> SummaryItems =
[
new("Presupuestos Activos", "128", "fas fa-file-signature", "accent-green"),
new("Remitos Emitidos", "42", "fas fa-truck-fast", "accent-orange"),
new("Facturas Pendientes", "15", "fas fa-file-invoice", "accent-teal"),
new("Tickets Abiertos", "6", "fas fa-headset", "accent-red")
new("Presupuestos activos", "128", "fas fa-file-signature", "accent-green"),
new("Remitos emitidos", "42", "fas fa-truck-fast", "accent-orange"),
new("Facturas pendientes", "15", "fas fa-file-invoice", "accent-teal"),
new("Tickets abiertos", "6", "fas fa-headset", "accent-red")
];
private readonly List<ActivityItem> RecentActivity =
@ -124,12 +156,10 @@
new("Ticket cerrado", "Hoy, 09:20", "fas fa-circle-check", "accent-red")
];
private void NavigateTo(string url)
{
Navigation.NavigateTo(url);
}
private void NavigateTo(string url) => Navigation.NavigateTo(url);
private sealed record QuickAccessItem(string Title, string Description, string Url, string Icon, string AccentClass);
private sealed record PrimaryAccessItem(string Title, string Description, string Url, string Icon, string Category, string Action, string ToneClass);
private sealed record ManagementAccessItem(string Title, string Description, string Url, string Icon, string AccentClass);
private sealed record SummaryItem(string Title, string Value, string Icon, string AccentClass);
private sealed record ActivityItem(string Title, string Description, string Icon, string AccentClass);
}

View File

@ -1,318 +1,466 @@
.home-dashboard {
display: flex;
flex-direction: column;
gap: 1.5rem;
padding-bottom: 2rem;
gap: clamp(2.2rem, 4vw, 3.75rem);
padding: .25rem 0 3rem;
}
.home-hero {
align-items: flex-start;
background: var(--background-highlight);
border: 1px solid var(--background-highlight-light);
border-radius: 8px;
.home-intro,
.section-heading {
display: flex;
gap: 1rem;
align-items: flex-end;
justify-content: space-between;
padding: 1.75rem;
gap: 2rem;
}
.home-eyebrow {
color: var(--link-color);
.home-intro h1 {
margin: .2rem 0 0;
color: var(--ph-text);
font-size: clamp(1.9rem, 3vw, 2.55rem);
font-weight: 760;
line-height: 1.12;
}
.home-intro p {
margin: .55rem 0 0;
font-size: 1rem;
}
.section-eyebrow {
display: block;
font-size: 0.8rem;
font-weight: 700;
letter-spacing: 0;
margin-bottom: 0.45rem;
color: var(--ph-primary);
font-size: .72rem;
font-weight: 750;
letter-spacing: .1em;
text-transform: uppercase;
}
.home-hero h1 {
color: var(--text-background);
font-size: 2rem;
font-weight: 700;
line-height: 1.2;
margin: 0;
}
.home-hero p {
color: var(--text-background);
font-size: 1rem;
line-height: 1.55;
margin: 0.75rem 0 0;
max-width: 760px;
opacity: 0.78;
}
.home-hero-status {
align-items: center;
background: var(--background);
border: 1px solid var(--background-highlight-light);
border-radius: 999px;
color: var(--text-background);
.platform-status {
display: inline-flex;
align-items: center;
flex: 0 0 auto;
font-size: 0.85rem;
font-weight: 600;
gap: 0.5rem;
padding: 0.45rem 0.75rem;
white-space: nowrap;
}
.home-status-dot {
background: #16a085;
gap: .55rem;
padding: .48rem .75rem;
color: var(--ph-text-muted);
background: var(--ph-surface);
border: 1px solid var(--ph-border);
border-radius: 999px;
box-shadow: 0 0 0 4px rgba(22, 160, 133, 0.14);
height: 0.6rem;
width: 0.6rem;
font-size: .78rem;
font-weight: 650;
box-shadow: var(--ph-shadow-sm);
}
.home-section-header {
align-items: baseline;
display: flex;
gap: 0.75rem;
justify-content: space-between;
.status-dot {
width: .52rem;
height: .52rem;
background: #35a58a;
border-radius: 50%;
box-shadow: 0 0 0 4px color-mix(in srgb, #35a58a 14%, transparent);
}
.home-section-header h2 {
color: var(--text-background);
font-size: 1.1rem;
font-weight: 700;
margin: 0;
}
.home-section-header span {
color: var(--text-background);
font-size: 0.82rem;
opacity: 0.62;
}
.quick-access-grid {
display: grid;
gap: 0.9rem;
grid-template-columns: repeat(4, minmax(0, 1fr));
}
.quick-access-card {
align-items: center;
background: var(--background-highlight);
border: 1px solid var(--background-highlight-light);
border-radius: 8px;
color: var(--text-background);
cursor: pointer;
display: grid;
gap: 0.75rem;
grid-template-columns: auto minmax(0, 1fr) auto;
min-height: 86px;
padding: 1rem;
text-align: left;
transition: border-color 0.15s ease, box-shadow 0.15s ease, transform 0.15s ease;
width: 100%;
}
.quick-access-card:hover,
.quick-access-card:focus {
border-color: var(--link-color);
box-shadow: 0 0.35rem 1rem rgba(0, 0, 0, 0.12);
outline: none;
transform: translateY(-1px);
}
.quick-access-icon,
.kpi-icon,
.activity-icon {
align-items: center;
border-radius: 8px;
color: #fff;
display: inline-flex;
flex: 0 0 auto;
justify-content: center;
}
.quick-access-icon {
font-size: 1rem;
height: 2.5rem;
width: 2.5rem;
}
.quick-access-content {
.dashboard-section {
display: flex;
flex-direction: column;
gap: 0.2rem;
min-width: 0;
gap: 1.15rem;
}
.quick-access-content strong {
color: var(--text-background);
font-size: 0.95rem;
line-height: 1.2;
.section-heading h2 {
margin: .25rem 0 0;
color: var(--ph-text);
font-size: clamp(1.25rem, 2vw, 1.55rem);
font-weight: 730;
}
.quick-access-content small {
color: var(--text-background);
font-size: 0.78rem;
line-height: 1.35;
opacity: 0.66;
.section-heading > p {
max-width: 38rem;
margin: 0 0 .12rem;
font-size: .9rem;
line-height: 1.45;
text-align: right;
}
.quick-access-arrow {
color: var(--text-background);
font-size: 0.8rem;
opacity: 0.48;
}
.home-content-grid {
.featured-grid {
display: grid;
gap: 1.5rem;
grid-template-columns: minmax(0, 1.3fr) minmax(280px, 0.7fr);
}
.home-content-grid section {
display: flex;
flex-direction: column;
gap: 0.9rem;
}
.kpi-grid {
display: grid;
gap: 0.9rem;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 1rem;
}
.kpi-card,
.activity-list {
background: var(--background-highlight);
border: 1px solid var(--background-highlight-light);
border-radius: 8px;
.featured-card,
.management-card {
position: relative;
color: var(--ph-text);
cursor: pointer;
text-align: left;
font: inherit;
}
.kpi-card {
align-items: center;
.featured-card {
display: flex;
gap: 0.9rem;
min-height: 92px;
padding: 1rem;
min-height: 18rem;
flex-direction: column;
justify-content: space-between;
padding: 1.55rem;
overflow: hidden;
background: linear-gradient(145deg, var(--ph-surface-raised), var(--ph-surface));
border: 1px solid var(--feature-border);
border-radius: var(--ph-radius-lg);
box-shadow: var(--ph-shadow-sm);
transition: transform .18s ease, border-color .18s ease, box-shadow .18s ease;
}
.kpi-icon {
font-size: 1rem;
height: 2.65rem;
width: 2.65rem;
.featured-card::after {
position: absolute;
right: -5rem;
bottom: -7rem;
width: 14rem;
height: 14rem;
background: radial-gradient(circle, var(--feature-glow), transparent 68%);
border-radius: 50%;
content: "";
pointer-events: none;
}
.kpi-card strong {
color: var(--text-background);
display: block;
font-size: 1.75rem;
.featured-card:hover,
.featured-card:focus-visible {
border-color: var(--feature-color);
box-shadow: var(--ph-shadow-md);
outline: none;
transform: translateY(-2px);
}
.tone-blue {
--feature-color: #719bea;
--feature-border: color-mix(in srgb, #719bea 52%, var(--ph-border));
--feature-glow: color-mix(in srgb, #719bea 17%, transparent);
}
.tone-green {
--feature-color: #55b7a4;
--feature-border: color-mix(in srgb, #55b7a4 52%, var(--ph-border));
--feature-glow: color-mix(in srgb, #55b7a4 17%, transparent);
}
.featured-card-topline {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 1rem;
}
.feature-icon,
.management-icon,
.metric-icon,
.activity-icon {
display: inline-flex;
align-items: center;
justify-content: center;
flex: 0 0 auto;
}
.feature-icon {
width: 3.3rem;
height: 3.3rem;
color: var(--feature-color);
background: color-mix(in srgb, var(--feature-color) 9%, var(--ph-surface));
border: 1px solid color-mix(in srgb, var(--feature-color) 35%, var(--ph-border));
border-radius: .75rem;
font-size: 1.2rem;
}
.feature-tag {
padding: .38rem .65rem;
color: var(--feature-color);
background: color-mix(in srgb, var(--feature-color) 9%, transparent);
border: 1px solid color-mix(in srgb, var(--feature-color) 28%, transparent);
border-radius: 999px;
font-size: .65rem;
font-weight: 750;
letter-spacing: .055em;
text-transform: uppercase;
}
.featured-copy {
display: flex;
flex-direction: column;
gap: .45rem;
max-width: 36rem;
margin: 1.5rem 0;
}
.featured-copy strong {
color: var(--ph-text);
font-size: 1.45rem;
line-height: 1.15;
}
.featured-copy small {
color: var(--ph-text-muted);
font-size: .9rem;
line-height: 1.55;
}
.card-action {
display: inline-flex;
align-items: center;
gap: .6rem;
z-index: 1;
color: var(--feature-color);
font-size: .84rem;
font-weight: 700;
}
.card-action i,
.management-arrow {
transition: transform .18s ease;
}
.featured-card:hover .card-action i,
.management-card:hover .management-arrow {
transform: translateX(3px);
}
.management-grid {
display: grid;
grid-template-columns: repeat(5, minmax(0, 1fr));
gap: .85rem;
}
.management-card {
display: grid;
min-height: 10rem;
align-content: start;
grid-template-columns: 2.55rem minmax(0, 1fr) auto;
gap: .9rem;
padding: 1.15rem;
background: var(--ph-surface);
border: 1px solid var(--ph-border);
border-radius: var(--ph-radius);
box-shadow: var(--ph-shadow-sm);
transition: transform .18s ease, border-color .18s ease, background-color .18s ease;
}
.management-card:hover,
.management-card:focus-visible {
background: var(--ph-surface-raised);
border-color: var(--ph-border-strong);
outline: none;
transform: translateY(-2px);
}
.management-icon {
width: 2.55rem;
height: 2.55rem;
color: var(--accent-color);
background: color-mix(in srgb, var(--accent-color) 9%, var(--ph-surface-subtle));
border: 1px solid color-mix(in srgb, var(--accent-color) 25%, var(--ph-border));
border-radius: .65rem;
}
.management-copy {
display: flex;
min-width: 0;
flex-direction: column;
gap: .32rem;
align-self: end;
grid-column: 1 / 4;
margin-top: 1rem;
}
.management-copy strong {
color: var(--ph-text);
font-size: .95rem;
}
.management-copy small {
color: var(--ph-text-muted);
font-size: .75rem;
line-height: 1.35;
}
.management-arrow {
align-self: center;
color: var(--ph-text-muted);
font-size: .75rem;
}
.section-heading-compact {
align-items: center;
}
.sample-label {
color: var(--ph-text-muted);
font-size: .72rem;
}
.overview-grid {
display: grid;
grid-template-columns: minmax(0, 1.35fr) minmax(20rem, .65fr);
gap: 1rem;
}
.metrics-panel {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
overflow: hidden;
background: var(--ph-surface);
border: 1px solid var(--ph-border);
border-radius: var(--ph-radius-lg);
box-shadow: var(--ph-shadow-sm);
}
.metric-item {
display: flex;
min-height: 7.5rem;
align-items: center;
gap: .9rem;
padding: 1.2rem;
}
.metric-item:nth-child(even) {
border-left: 1px solid var(--ph-border);
}
.metric-item:nth-child(n+3) {
border-top: 1px solid var(--ph-border);
}
.metric-icon,
.activity-icon {
color: var(--accent-color);
background: color-mix(in srgb, var(--accent-color) 10%, var(--ph-surface-subtle));
border: 1px solid color-mix(in srgb, var(--accent-color) 24%, var(--ph-border));
}
.metric-icon {
width: 2.8rem;
height: 2.8rem;
border-radius: .7rem;
}
.metric-copy {
display: flex;
min-width: 0;
flex-direction: column;
gap: .28rem;
}
.metric-copy strong {
color: var(--ph-text);
font-size: 1.65rem;
line-height: 1;
}
.kpi-card span {
color: var(--text-background);
display: block;
font-size: 0.85rem;
margin-top: 0.35rem;
opacity: 0.68;
.metric-copy small {
color: var(--ph-text-muted);
font-size: .78rem;
}
.activity-list {
display: flex;
flex-direction: column;
.activity-panel {
overflow: hidden;
background: var(--ph-surface);
border: 1px solid var(--ph-border);
border-radius: var(--ph-radius-lg);
box-shadow: var(--ph-shadow-sm);
}
.activity-header {
display: flex;
align-items: center;
justify-content: space-between;
gap: 1rem;
padding: 1.1rem 1.2rem;
background: var(--ph-surface-subtle);
border-bottom: 1px solid var(--ph-border);
}
.activity-header h3 {
margin: 0;
color: var(--ph-text);
font-size: .95rem;
}
.activity-header p {
margin: .2rem 0 0;
font-size: .72rem;
}
.activity-header > i {
color: var(--ph-text-muted);
}
.activity-item {
align-items: center;
display: flex;
gap: 0.85rem;
min-height: 72px;
padding: 0.95rem 1rem;
min-height: 4.2rem;
align-items: center;
gap: .75rem;
padding: .75rem 1.2rem;
}
.activity-item + .activity-item {
border-top: 1px solid var(--background-highlight-light);
border-top: 1px solid var(--ph-border);
}
.activity-icon {
font-size: 0.9rem;
height: 2.25rem;
width: 2.25rem;
width: 2.1rem;
height: 2.1rem;
border-radius: .55rem;
font-size: .75rem;
}
.activity-item strong,
.activity-item small {
display: block;
}
.activity-item strong {
color: var(--text-background);
display: block;
font-size: 0.92rem;
line-height: 1.25;
color: var(--ph-text);
font-size: .8rem;
}
.activity-item small {
color: var(--text-background);
display: block;
font-size: 0.78rem;
margin-top: 0.18rem;
opacity: 0.62;
margin-top: .15rem;
color: var(--ph-text-muted);
font-size: .7rem;
}
.accent-blue {
background: #2473a6;
}
.accent-green {
background: #138f77;
}
.accent-indigo {
background: #34495e;
}
.accent-orange {
background: #da8c10;
}
.accent-teal {
background: #2c8f9e;
}
.accent-violet {
background: #7f3d9b;
}
.accent-red {
background: #cf4435;
}
.accent-blue { --accent-color: #5288c7; }
.accent-green { --accent-color: #49a38b; }
.accent-indigo { --accent-color: #7186ab; }
.accent-orange { --accent-color: #d59a34; }
.accent-teal { --accent-color: #55a0b0; }
.accent-violet { --accent-color: #8d62b5; }
.accent-red { --accent-color: #cc604e; }
@media (max-width: 1199.98px) {
.quick-access-grid {
grid-template-columns: repeat(3, minmax(0, 1fr));
}
.management-grid { grid-template-columns: repeat(3, minmax(0, 1fr)); }
}
@media (max-width: 991.98px) {
.home-content-grid {
grid-template-columns: 1fr;
}
.quick-access-grid {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.overview-grid { grid-template-columns: 1fr; }
}
@media (max-width: 767.98px) {
.home-hero {
flex-direction: column;
padding: 1.25rem;
}
.home-hero h1 {
font-size: 1.6rem;
}
.home-hero-status {
white-space: normal;
}
.kpi-grid,
.quick-access-grid {
grid-template-columns: 1fr;
}
.home-dashboard { gap: 2.5rem; }
.home-intro,
.section-heading { align-items: flex-start; flex-direction: column; gap: .8rem; }
.section-heading > p { max-width: none; text-align: left; }
.featured-grid,
.management-grid { grid-template-columns: 1fr; }
.featured-card { min-height: 15rem; }
.management-card { min-height: 8.5rem; }
}
@media (max-width: 479.98px) {
.platform-status { align-self: flex-start; }
.metrics-panel { grid-template-columns: 1fr; }
.metric-item:nth-child(even) { border-left: 0; }
.metric-item:nth-child(n+2) { border-top: 1px solid var(--ph-border); }
.feature-tag { max-width: 9.5rem; text-align: center; }
}
@media (prefers-reduced-motion: reduce) {
.featured-card,
.management-card,
.card-action i,
.management-arrow { transition: none; }
}

View File

@ -1,13 +1,14 @@
@using System.Security.Claims
<AuthorizeView>
<Authorized Context="authContext">
<div class="d-flex justify-content-end align-items-center gap-3 px-3 py-2">
<span class="text-dark">
Bienvenido, @GetUserDisplayName(authContext.User)
<div class="login-state">
<span class="user-avatar" aria-hidden="true">@GetInitial(authContext.User)</span>
<span class="user-copy">
<small>Sesión activa</small>
<strong>@GetUserDisplayName(authContext.User)</strong>
</span>
<a href="Logout" class="btn btn-sm btn-danger rounded-3 d-flex align-items-center gap-1 text-decoration-none">
<a href="Logout" class="logout-button" title="Cerrar sesión" aria-label="Cerrar sesión">
<i class="oi oi-account-logout"></i>
<span>Cerrar sesión</span>
</a>
</div>
</Authorized>
@ -19,4 +20,9 @@
?? user.Identity?.Name
?? "Usuario";
}
private string GetInitial(ClaimsPrincipal user)
{
var displayName = GetUserDisplayName(user).Trim();
return displayName.Length > 0 ? displayName[..1].ToUpperInvariant() : "U";
}
}

View File

@ -1,18 +1,8 @@
/* .btn-logout {
text-decoration: none !important;
margin-left: 20px;
padding: 8px 8px 8px 8px;
background-image: linear-gradient(90deg, rgb(169, 15, 144) 0%, rgb(23, 25, 85) 90%);
border-radius: 4px 12px 12px 12px;
color: white;
cursor: pointer;
} */
.btn-logout {
text-decoration: none !important;
background-color: rgb(39, 54, 97); /* Azul oscuro (igual al fondo) */
color: white;
padding: 8px 8px 8px 8px;
border: 2px solid rgba(255, 255, 255, 0.2); /* Borde sutil */
border-radius: 0.5em;
cursor: pointer;
}
.login-state { display: flex; min-width: 0; align-items: center; gap: .65rem; }
.user-avatar { display: inline-flex; width: 2.2rem; height: 2.2rem; flex: 0 0 2.2rem; align-items: center; justify-content: center; color: #fff; background: #3157a4; border-radius: 50%; font-size: .78rem; font-weight: 750; }
.user-copy { display: flex; min-width: 0; flex-direction: column; line-height: 1.15; text-align: left; }
.user-copy small { color: var(--ph-text-muted); font-size: .66rem; }
.user-copy strong { max-width: 13rem; overflow: hidden; color: var(--ph-text); font-size: .82rem; text-overflow: ellipsis; white-space: nowrap; }
.logout-button { display: inline-flex; width: 2.2rem; height: 2.2rem; align-items: center; justify-content: center; color: var(--ph-text-muted); border: 1px solid var(--ph-border); border-radius: .55rem; text-decoration: none; transition: color .15s ease, background-color .15s ease, border-color .15s ease; }
.logout-button:hover { color: var(--ph-danger); background: color-mix(in srgb, var(--ph-danger) 8%, transparent); border-color: color-mix(in srgb, var(--ph-danger) 35%, var(--ph-border)); }
@media (max-width: 575.98px) { .user-copy { display: none; } }

View File

@ -1,24 +1,23 @@
@if( Data is not null && Data.Any())
{
<div class="container">
<div class="row">
<div class="ph-table-toolbar">
<div class="ph-search-area">
<!-- TITULO TABLA-->
<div class="col-md-10">
@if (ShowQuickSearch)
{
<div class="row mb-3">
<div class="col-md-6">
<InputText type="text" class="form-control" placeholder="Búsqueda rápida..." @bind-Value="SearchTerm"/>
</div>
</div>
}
</div>
@if (ShowQuickSearch)
{
<label for="ph-quick-search" class="visually-hidden">Búsqueda rápida</label>
<div class="ph-search-input">
<i class="fas fa-magnifying-glass" aria-hidden="true"></i>
<InputText id="ph-quick-search" type="search" class="form-control" placeholder="Buscar en la tabla…" @bind-Value="SearchTerm"/>
</div>
}
</div>
<!-- CONTROLES DE PAGINACION-->
<div class="col-md-2">
<div class="ph-pagination-area">
@if (ShowPageButtons)
{
<nav aria-label="Page Navigation">
<nav aria-label="Paginación de resultados">
<ul class="pagination justify-content-end">
<li class="page-item @(CurrentPage == 1 ? "disabled" : "")">
<a class="page-link" href="#" tabindex="-1" @onclick:preventDefault @onclick="GoToPreviousPage">Anterior</a>
@ -71,10 +70,10 @@
</nav>
}
</div>
</div>
</div>
<!-- RENDERIZACION DE TABLA-->
<table class="table table-sm table-hover">
<div class="ph-table-scroll">
<table class="table table-hover">
<!-- RENDERIZAR ENCABEZADOS-->
<thead>
<tr>
@ -90,7 +89,7 @@
{
if (!column.Contains("Hide:"))
{
<th @onclick="()=>SortbyColumn(column)" style="cursor:pointer;user-select:none;" class="ph-header">
<th @onclick="()=>SortbyColumn(column)" class="ph-header @(SorteableCols.Contains(column) ? "sortable" : string.Empty)">
@column
@if (column==SortedColumn)
{
@ -102,7 +101,7 @@
<!-- RENDERIZAR ENCABEZADO DE COLUMNA TIPO BUTTON-->
@if (RenderButtons)
{
<th style="cursor:default; user-select:none;" class="ph-header">Acciones</th>
<th class="ph-header actions-header">Acciones</th>
}
</tr>
</thead>
@ -115,7 +114,7 @@
<!-- RENDERIZAR COLUMNA DE SELECCION-->
@if (RenderSelect)
{
<td>
<td class="ph-actions">
@{
string rowId = string.Empty;
if (item.TryGetValue(SelectionField, out var idObj) && idObj is not null)
@ -199,6 +198,15 @@
}
</tbody>
</table>
</div>
}
else
{
<div class="empty-state ph-empty-state">
<i class="fas fa-inbox" aria-hidden="true"></i>
<strong>Sin resultados</strong>
<span>No hay información disponible para mostrar.</span>
</div>
}
@code {

View File

@ -1,16 +1,16 @@
ph-header {
color: whitesmoke;
}
/*.ph-container {
overflow: auto;
height: 460px;
}
.ph-container thead th {
position:sticky;
top:0;
z-index:1;
}*/
td, th {
padding: 5px
}
.ph-table-toolbar { display: flex; align-items: center; justify-content: space-between; gap: 1rem; margin-bottom: .85rem; }
.ph-search-area { width: min(100%, 27rem); }
.ph-search-input { position: relative; }
.ph-search-input > i { position: absolute; top: 50%; left: .9rem; z-index: 1; color: var(--ph-text-muted); font-size: .8rem; transform: translateY(-50%); pointer-events: none; }
.ph-search-input ::deep input { padding-left: 2.45rem; }
.ph-pagination-area { margin-left: auto; }
.ph-header.sortable { cursor: pointer; user-select: none; }
.ph-header.sortable:hover { color: var(--ph-primary); background: var(--ph-primary-soft); }
.actions-header { cursor: default; user-select: none; }
.ph-actions { display: flex; flex-wrap: wrap; gap: .4rem; min-width: 7rem; }
.ph-actions ::deep .btn { margin: 0 !important; }
.ph-empty-state { display: flex; flex-direction: column; align-items: center; gap: .35rem; }
.ph-empty-state i { margin-bottom: .25rem; color: var(--ph-primary); font-size: 1.5rem; }
.ph-empty-state strong { color: var(--ph-text); }
.ph-empty-state span { font-size: .84rem; }
@media (max-width: 767.98px) { .ph-table-toolbar { align-items: stretch; flex-direction: column; } .ph-search-area { width: 100%; } .ph-pagination-area { max-width: 100%; margin-left: 0; overflow-x: auto; } }

View File

@ -1,16 +1,16 @@

<div class="toggle-container">
<label class="switch">
<input type="checkbox" class="switch" @bind=ToggleState />
<label class="switch" title="Cambiar tema visual">
<input type="checkbox" class="switch" aria-label="Activar tema oscuro" @bind=ToggleState />
<span class="slider round"></span>
</label>
@if (!string.IsNullOrEmpty(Caption))
{
<span class="toggle-label">@Caption</span>
}
<label style="margin:10px;">
<span class="toggle-icon">
@ChildContent
</label>
</span>
</div>
@code {
[Parameter]

View File

@ -1,74 +1,11 @@
/* Begin Toggle Switch*/
.toggle-container {
display: flex;
align-items: center;
}
.toggle-label {
margin-left: 15px; /* Ajusta el espaciado según sea necesario */
font-size: 130%;
}
/* The switch - the box around the slider */
.switch {
position: relative;
display: inline-block;
width: 70px;
height: 34px;
}
/* Hide default HTML checkbox */
.switch input {
opacity: 0;
width: 0;
height: 0;
}
/* The slider */
.slider {
position: absolute;
cursor: pointer;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: #ccc;
-webkit-transition: .4s;
transition: .4s;
}
.slider:before {
position: absolute;
content: "";
height: 26px;
width: 26px;
left: 4px;
bottom: 4px;
background-color: white;
-webkit-transition: .4s;
transition: .4s;
}
input:checked + .slider {
background-color: #6610f2;
}
input:focus + .slider {
box-shadow: 0 0 1px #6610f2;
}
input:checked + .slider:before {
-webkit-transform: translateX(36px);
-ms-transform: translateX(36px);
transform: translateX(36px);
}
/* Rounded sliders */
.slider.round {
border-radius: 34px;
}
.slider.round:before {
border-radius: 50%;
}
.toggle-label {
margin-left: 3px; /* Ajusta el espaciado según sea necesario */
}
/*End Toggle Switch*/
.toggle-container { display: flex; align-items: center; gap: .5rem; color: var(--ph-text-muted); }
.switch { position: relative; display: inline-block; width: 2.45rem; height: 1.4rem; margin: 0; }
.switch input { width: 0; height: 0; opacity: 0; }
.slider { position: absolute; inset: 0; cursor: pointer; background: var(--ph-border-strong); border-radius: 999px; transition: background-color .2s ease; }
.slider::before { position: absolute; bottom: .2rem; left: .2rem; width: 1rem; height: 1rem; content: ""; background: #fff; border-radius: 50%; box-shadow: 0 1px 3px rgba(0, 0, 0, .25); transition: transform .2s ease; }
input:checked + .slider { background: #3157a4; }
input:focus-visible + .slider { box-shadow: 0 0 0 .22rem var(--ph-focus); }
input:checked + .slider::before { transform: translateX(1.05rem); }
.toggle-label { color: var(--ph-text-muted); font-size: .76rem; font-weight: 600; white-space: nowrap; }
.toggle-icon { display: inline-flex; width: 1rem; justify-content: center; font-size: .85rem; }
@media (max-width: 575.98px) { .toggle-label { display: none; } }

View File

@ -1,181 +1,128 @@
@import url('open-iconic/font/css/open-iconic-bootstrap.min.css');
:root {
--background: white;
--text-background: black;
--background-highlight: #f7f7f7;
--background-highlight-light: #e2e3e5;
--link-color: #0366d6;
color-scheme: light;
--ph-primary: #3157a4; --ph-primary-hover: #274887; --ph-primary-soft: #e9effb;
--ph-accent: #159b8c; --ph-danger: #c33d4a; --ph-warning: #b86f0b; --ph-success: #18785d; --ph-info: #2476a8;
--ph-canvas: #f4f6fa; --ph-surface: #fff; --ph-surface-subtle: #f8fafc; --ph-surface-raised: #fff;
--ph-border: #dfe5ee; --ph-border-strong: #c8d1df; --ph-text: #1d2939; --ph-text-muted: #667085;
--ph-sidebar: #202f55; --ph-sidebar-deep: #182442; --ph-sidebar-text: #dce4f3; --ph-sidebar-muted: #9facc4;
--ph-focus: rgba(49, 87, 164, .22); --ph-shadow-sm: 0 1px 2px rgba(16, 24, 40, .05); --ph-shadow-md: 0 10px 30px rgba(16, 24, 40, .08);
--ph-radius-sm: .5rem; --ph-radius: .75rem; --ph-radius-lg: 1rem;
--background: var(--ph-surface); --text-background: var(--ph-text); --background-highlight: var(--ph-surface-subtle);
--background-highlight-light: var(--ph-border); --link-color: var(--ph-primary);
}
.dark-mode {
--background: #1C1B1F;
--text-background: white;
--background-highlight: #202124;
--background-highlight-light: #3C4043;
--link-color: #3ca4ff;
}
.nav-flex-column {
list-style-type: square;
padding-left: 20px; /* Alineación adecuada para submenús */
color-scheme: dark;
--ph-primary: #8daef2; --ph-primary-hover: #abc2f3; --ph-primary-soft: #263a63;
--ph-accent: #52c7b8; --ph-danger: #f27a84; --ph-warning: #f1b85c; --ph-success: #63c5a5; --ph-info: #74b9e4;
--ph-canvas: #101725; --ph-surface: #182131; --ph-surface-subtle: #1d283a; --ph-surface-raised: #202c40;
--ph-border: #303d52; --ph-border-strong: #435068; --ph-text: #edf2f8; --ph-text-muted: #aab6c8;
--ph-sidebar: #121b2e; --ph-sidebar-deep: #0e1626; --ph-sidebar-text: #e4eaf4; --ph-sidebar-muted: #91a0ba;
--ph-focus: rgba(141, 174, 242, .28); --ph-shadow-sm: 0 1px 2px rgba(0, 0, 0, .25); --ph-shadow-md: 0 12px 32px rgba(0, 0, 0, .28);
}
.nav-item {
padding-top: 0.5rem;
padding-bottom: 0.5rem;
}
*, *::before, *::after { box-sizing: border-box; }
html, body { min-height: 100%; font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; background: var(--ph-canvas); color: var(--ph-text); -webkit-font-smoothing: antialiased; }
body { margin: 0; }
a, .btn-link { color: var(--link-color); }
a { text-underline-offset: .18em; }
h1, h2, h3, h4, h5, h6 { color: var(--ph-text); letter-spacing: -.018em; }
h1:focus { outline: none; }
p { color: var(--ph-text-muted); }
main { min-width: 0; background: var(--ph-canvas); color: var(--ph-text); }
::selection { background: var(--ph-primary-soft); color: var(--ph-text); }
.content { padding-top: 1.5rem; padding-bottom: 2.5rem; }
.content > * { max-width: 1600px; margin-inline: auto; }
.nav-link label {
font-size: 0.875rem;
}
.card { overflow: hidden; color: var(--ph-text); background: var(--ph-surface); border: 1px solid var(--ph-border); border-radius: var(--ph-radius-lg); box-shadow: var(--ph-shadow-sm); }
.card-header, .card-footer { min-height: 4rem; padding: 1rem 1.25rem; color: var(--ph-text); background: var(--ph-surface); border-color: var(--ph-border); }
.card-header { border-bottom: 1px solid var(--ph-border); }
.card-footer { border-top: 1px solid var(--ph-border); }
.card-body { padding: 1.25rem; color: var(--ph-text); background: var(--ph-surface); }
.card-title { font-size: 1.15rem; font-weight: 700; }
html, body {
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
}
.btn { min-height: 2.5rem; padding: .48rem .9rem; border-radius: var(--ph-radius-sm); font-weight: 600; box-shadow: none; transition: transform .15s ease, box-shadow .15s ease, background-color .15s ease, border-color .15s ease; }
.btn:hover:not(:disabled) { transform: translateY(-1px); }
.btn:active:not(:disabled) { transform: translateY(0); }
.btn-sm { min-height: 2.1rem; padding: .35rem .7rem; }
.btn-primary { color: #fff; background: #3157a4; border-color: #3157a4; }
.btn-primary:hover, .btn-primary:focus { color: #fff; background: #274887; border-color: #274887; }
.btn-success { color: #fff; background: #18785d; border-color: #18785d; }
.btn-danger { color: #fff; background: #b93845; border-color: #b93845; }
.btn-warning { color: #2b1a02; background: #f0b24d; border-color: #f0b24d; }
.btn-secondary { color: var(--ph-text); background: var(--ph-surface-subtle); border-color: var(--ph-border-strong); }
.btn-secondary:hover { color: var(--ph-text); background: var(--ph-border); border-color: var(--ph-border-strong); }
.btn-outline-primary { color: var(--ph-primary); border-color: var(--ph-primary); }
.btn-outline-primary:hover { color: #fff; background: #3157a4; border-color: #3157a4; }
.btn.rounded-pill { border-radius: var(--ph-radius-sm) !important; }
.btn:disabled { opacity: .58; cursor: not-allowed; }
h1:focus {
outline: none;
}
.form-label, label { margin-bottom: .42rem; color: var(--ph-text); font-size: .875rem; font-weight: 600; }
.form-control, .form-select, select, textarea, input:not([type="checkbox"]):not([type="radio"]):not([type="button"]):not([type="submit"]) { min-height: 2.65rem; color: var(--ph-text); background-color: var(--ph-surface); border: 1px solid var(--ph-border-strong); border-radius: var(--ph-radius-sm); box-shadow: none; }
.form-control::placeholder, textarea::placeholder, input::placeholder { color: var(--ph-text-muted); opacity: .78; }
.form-control:focus, .form-select:focus, select:focus, textarea:focus, input:focus { color: var(--ph-text); background-color: var(--ph-surface); border-color: var(--ph-primary); outline: 0; box-shadow: 0 0 0 .22rem var(--ph-focus); }
.form-control:disabled, .form-select:disabled, input:disabled, select:disabled, textarea:disabled { color: var(--ph-text-muted); background: var(--ph-surface-subtle); }
.form-check-input { background-color: var(--ph-surface); border-color: var(--ph-border-strong); }
.form-check-input:checked { background-color: #3157a4; border-color: #3157a4; }
.row + .row { margin-top: 1rem; }
.row > [class*="col-"] > label:first-child { display: inline-block; }
hr { margin: 1.5rem 0; border-color: var(--ph-border); opacity: 1; }
.valid.modified:not([type=checkbox]) { border-color: var(--ph-success); outline: none; }
.invalid { border-color: var(--ph-danger) !important; outline: none; }
.validation-message { display: block; margin-top: .3rem; color: var(--ph-danger); font-size: .82rem; }
.validation-errors { margin: 0 0 1rem; padding: .85rem 1rem .85rem 2rem; color: var(--ph-danger); background: color-mix(in srgb, var(--ph-danger) 9%, transparent); border: 1px solid color-mix(in srgb, var(--ph-danger) 28%, transparent); border-radius: var(--ph-radius-sm); }
.nav-link:hover {
background-color: rgba(255, 255, 255, 0.1);
color: #fff;
transition: background-color 0.2s;
}
.table-responsive, .ph-table-scroll { overflow-x: auto; border: 1px solid var(--ph-border); border-radius: var(--ph-radius); }
.table { margin-bottom: 0; color: var(--ph-text); background: var(--ph-surface); border-color: var(--ph-border); vertical-align: middle; }
.table > :not(caption) > * > * { padding: .78rem .85rem; color: inherit; background: transparent; border-bottom-color: var(--ph-border); box-shadow: none; }
.table thead th { color: var(--ph-text-muted); background: var(--ph-surface-subtle); font-size: .75rem; font-weight: 700; letter-spacing: .035em; text-transform: uppercase; white-space: nowrap; }
.table tbody tr { transition: background-color .12s ease; }
.table-hover > tbody > tr:hover > * { --bs-table-accent-bg: transparent; color: var(--ph-text); background: var(--ph-primary-soft); }
.table-bordered > :not(caption) > * { border-color: var(--ph-border); }
.pagination { flex-wrap: wrap; gap: .25rem; margin-bottom: 0; }
.page-link { min-width: 2.2rem; color: var(--ph-text-muted); background: var(--ph-surface); border: 1px solid var(--ph-border); border-radius: .45rem !important; text-align: center; }
.page-link:hover { color: var(--ph-primary); background: var(--ph-primary-soft); border-color: var(--ph-primary); }
.page-item.active .page-link { color: #fff; background: #3157a4; border-color: #3157a4; }
.page-item.disabled .page-link { color: var(--ph-text-muted); background: var(--ph-surface-subtle); border-color: var(--ph-border); opacity: .55; }
.nav-item {
margin-bottom: 0.25rem;
}
a, .btn-link {
/*color: #0071c1;*/
color: var(--link-color);
}
.alert-secondary {
background-color: var(--background-highlight-light);
color: var(--text-background);
}
.table {
color: var(--text-background);
background-color: var(--background);
}
main {
background-color: var(--background);
color: var(--text-background);
}
.card-header {
background-color: var(--background-highlight-light);
color: var(--text-background);
}
.badge { padding: .42em .65em; border-radius: 999px; font-weight: 650; }
.alert { color: var(--ph-text); background: var(--ph-surface-subtle); border-color: var(--ph-border); border-radius: var(--ph-radius); }
.alert-secondary { color: var(--ph-text); background: var(--ph-surface-subtle); }
.modal-content, .blazored-modal { color: var(--ph-text); background: var(--ph-surface-raised) !important; border: 1px solid var(--ph-border) !important; border-radius: var(--ph-radius-lg) !important; box-shadow: var(--ph-shadow-md) !important; }
.modal-header, .modal-footer { border-color: var(--ph-border); }
.modal-body { padding: 1.35rem; }
.modal-footer { gap: .5rem; padding: 1rem 1.35rem; background: var(--ph-surface-subtle); }
.blazored-modal-overlay { background-color: rgba(8, 15, 30, .62) !important; backdrop-filter: blur(2px); }
.blazored-toast { color: var(--ph-text) !important; background: var(--ph-surface-raised) !important; border: 1px solid var(--ph-border) !important; border-radius: var(--ph-radius) !important; box-shadow: var(--ph-shadow-md) !important; }
.blazored-toast-heading { color: var(--ph-text) !important; }
.blazored-toast-message { color: var(--ph-text-muted) !important; }
.empty-state { padding: 2.5rem 1rem; color: var(--ph-text-muted); text-align: center; background: var(--ph-surface-subtle); border: 1px dashed var(--ph-border-strong); border-radius: var(--ph-radius); }
.spinner-border { color: var(--ph-primary); }
.card-body {
background-color: var(--background-highlight);
color: var(--text-background);
#blazor-error-ui { position: fixed; right: 1rem; bottom: 1rem; left: 1rem; z-index: 2000; display: none; max-width: 50rem; margin-inline: auto; padding: 1rem 3rem 1rem 1rem; color: #fff; background: #8f2833; border-radius: var(--ph-radius); box-shadow: var(--ph-shadow-md); }
#blazor-error-ui a { color: #fff; }
#blazor-error-ui .dismiss { position: absolute; top: .8rem; right: 1rem; cursor: pointer; }
.blazor-error-boundary { padding: 1rem; color: #fff; background: #8f2833; border-radius: var(--ph-radius); }
.blazor-error-boundary::after { content: "Ocurrió un error inesperado."; }
.loading-progress { position: relative; display: block; width: 5.5rem; height: 5.5rem; margin: 28vh auto 1rem; }
.loading-progress circle { fill: none; stroke: var(--ph-border); stroke-width: .48rem; transform: rotate(-90deg); transform-origin: 50% 50%; }
.loading-progress circle:last-child { stroke: #3157a4; stroke-dasharray: calc(3.141 * var(--blazor-load-percentage, 0%) * .8), 500%; transition: stroke-dasharray .05s ease-in-out; }
.loading-progress-text { position: absolute; inset: calc(28vh + 2.15rem) 0 auto; color: var(--ph-text-muted); font-size: .8rem; font-weight: 700; text-align: center; }
.loading-progress-text::after { content: var(--blazor-load-percentage-text, "Cargando"); }
::-webkit-scrollbar { width: 10px; height: 10px; }
::-webkit-scrollbar-track { background: transparent; }
::-webkit-scrollbar-thumb { background: color-mix(in srgb, var(--ph-text-muted) 35%, transparent); border: 2px solid transparent; border-radius: 999px; background-clip: content-box; }
::-webkit-scrollbar-thumb:hover { background: color-mix(in srgb, var(--ph-text-muted) 60%, transparent); background-clip: content-box; }
@media (max-width: 767.98px) {
.content { padding: 1rem .85rem 2rem !important; }
.card-header, .card-body, .card-footer { padding: 1rem; }
.card-header { min-height: auto; }
.card-header.d-flex, .card-footer .d-flex, .card-body > .d-flex { flex-wrap: wrap; gap: .65rem; }
.card-body .row { row-gap: 1rem; }
.table { min-width: 680px; }
.btn { max-width: 100%; }
}
.card-footer {
background-color: var(--background-highlight-light);
color: var(--text-background);
}
.btn-primary {
color: #fff;
background-color: #1b6ec2;
border-color: #1861ac;
}
.btn:focus, .btn:active:focus, .btn-link.nav-link:focus, .form-control:focus, .form-check-input:focus {
box-shadow: 0 0 0 0.1rem white, 0 0 0 0.25rem #258cfb;
}
.content {
padding-top: 1.1rem;
}
.valid.modified:not([type=checkbox]) {
outline: 1px solid #26b050;
}
.invalid {
outline: 1px solid red;
}
.validation-message {
color: red;
}
#blazor-error-ui {
background: lightyellow;
bottom: 0;
box-shadow: 0 -1px 2px rgba(0, 0, 0, 0.2);
display: none;
left: 0;
padding: 0.6rem 1.25rem 0.7rem 1.25rem;
position: fixed;
width: 100%;
z-index: 1000;
}
#blazor-error-ui .dismiss {
cursor: pointer;
position: absolute;
right: 0.75rem;
top: 0.5rem;
}
.blazor-error-boundary {
background: url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNTYiIGhlaWdodD0iNDkiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIG92ZXJmbG93PSJoaWRkZW4iPjxkZWZzPjxjbGlwUGF0aCBpZD0iY2xpcDAiPjxyZWN0IHg9IjIzNSIgeT0iNTEiIHdpZHRoPSI1NiIgaGVpZ2h0PSI0OSIvPjwvY2xpcFBhdGg+PC9kZWZzPjxnIGNsaXAtcGF0aD0idXJsKCNjbGlwMCkiIHRyYW5zZm9ybT0idHJhbnNsYXRlKC0yMzUgLTUxKSI+PHBhdGggZD0iTTI2My41MDYgNTFDMjY0LjcxNyA1MSAyNjUuODEzIDUxLjQ4MzcgMjY2LjYwNiA1Mi4yNjU4TDI2Ny4wNTIgNTIuNzk4NyAyNjcuNTM5IDUzLjYyODMgMjkwLjE4NSA5Mi4xODMxIDI5MC41NDUgOTIuNzk1IDI5MC42NTYgOTIuOTk2QzI5MC44NzcgOTMuNTEzIDI5MSA5NC4wODE1IDI5MSA5NC42NzgyIDI5MSA5Ny4wNjUxIDI4OS4wMzggOTkgMjg2LjYxNyA5OUwyNDAuMzgzIDk5QzIzNy45NjMgOTkgMjM2IDk3LjA2NTEgMjM2IDk0LjY3ODIgMjM2IDk0LjM3OTkgMjM2LjAzMSA5NC4wODg2IDIzNi4wODkgOTMuODA3MkwyMzYuMzM4IDkzLjAxNjIgMjM2Ljg1OCA5Mi4xMzE0IDI1OS40NzMgNTMuNjI5NCAyNTkuOTYxIDUyLjc5ODUgMjYwLjQwNyA1Mi4yNjU4QzI2MS4yIDUxLjQ4MzcgMjYyLjI5NiA1MSAyNjMuNTA2IDUxWk0yNjMuNTg2IDY2LjAxODNDMjYwLjczNyA2Ni4wMTgzIDI1OS4zMTMgNjcuMTI0NSAyNTkuMzEzIDY5LjMzNyAyNTkuMzEzIDY5LjYxMDIgMjU5LjMzMiA2OS44NjA4IDI1OS4zNzEgNzAuMDg4N0wyNjEuNzk1IDg0LjAxNjEgMjY1LjM4IDg0LjAxNjEgMjY3LjgyMSA2OS43NDc1QzI2Ny44NiA2OS43MzA5IDI2Ny44NzkgNjkuNTg3NyAyNjcuODc5IDY5LjMxNzkgMjY3Ljg3OSA2Ny4xMTgyIDI2Ni40NDggNjYuMDE4MyAyNjMuNTg2IDY2LjAxODNaTTI2My41NzYgODYuMDU0N0MyNjEuMDQ5IDg2LjA1NDcgMjU5Ljc4NiA4Ny4zMDA1IDI1OS43ODYgODkuNzkyMSAyNTkuNzg2IDkyLjI4MzcgMjYxLjA0OSA5My41Mjk1IDI2My41NzYgOTMuNTI5NSAyNjYuMTE2IDkzLjUyOTUgMjY3LjM4NyA5Mi4yODM3IDI2Ny4zODcgODkuNzkyMSAyNjcuMzg3IDg3LjMwMDUgMjY2LjExNiA4Ni4wNTQ3IDI2My41NzYgODYuMDU0N1oiIGZpbGw9IiNGRkU1MDAiIGZpbGwtcnVsZT0iZXZlbm9kZCIvPjwvZz48L3N2Zz4=) no-repeat 1rem/1.8rem, #b32121;
padding: 1rem 1rem 1rem 3.7rem;
color: white;
}
.blazor-error-boundary::after {
content: "An error has occurred."
}
.loading-progress {
position: relative;
display: block;
width: 8rem;
height: 8rem;
margin: 20vh auto 1rem auto;
}
.loading-progress circle {
fill: none;
stroke: #e0e0e0;
stroke-width: 0.6rem;
transform-origin: 50% 50%;
transform: rotate(-90deg);
}
.loading-progress circle:last-child {
stroke: #1b6ec2;
stroke-dasharray: calc(3.141 * var(--blazor-load-percentage, 0%) * 0.8), 500%;
transition: stroke-dasharray 0.05s ease-in-out;
}
.loading-progress-text {
position: absolute;
text-align: center;
font-weight: bold;
inset: calc(20vh + 3.25rem) 0 auto 0.2rem;
}
.loading-progress-text:after {
content: var(--blazor-load-percentage-text, "Loading");
}
/* Cambiar el color del scrollbar con transparencia */
::-webkit-scrollbar {
width: 12px;
}
::-webkit-scrollbar-track {
background-color: rgba(241, 241, 241, 0); /* Fondo semi transparente */
}
::-webkit-scrollbar-thumb {
background-color: rgba(136, 136, 136, 0.2); /* Pulgar semi transparente */
border-radius: 6px;
}
::-webkit-scrollbar-thumb:hover {
background-color: rgba(100, 100, 100, 0.5); /* Cambio al pasar el mouse semi transparente */
}
@media (prefers-reduced-motion: reduce) { *, *::before, *::after { scroll-behavior: auto !important; transition-duration: .01ms !important; animation-duration: .01ms !important; } }

View File

Before

Width:  |  Height:  |  Size: 224 KiB

After

Width:  |  Height:  |  Size: 224 KiB

View File

Before

Width:  |  Height:  |  Size: 192 KiB

After

Width:  |  Height:  |  Size: 192 KiB

View File

@ -1,17 +1,18 @@
<!DOCTYPE html>
<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
<title>phronCare.UIBlazor</title>
<title>PhronCare · SaludLAB</title>
<base href="/" />
<!-- Estilos propios -->
<link href="css/bootstrap/bootstrap.min.css" rel="stylesheet" />
<link href="css/app.css" rel="stylesheet" />
<link href="css/fontawesome-free-6.4.2-web/css/all.min.css" rel="stylesheet" />
<link rel="icon" type="image/png" href="favicon.png" />
<link rel="icon" type="image/png" href="images/SaludLAB.png" />
<link rel="apple-touch-icon" href="images/SaludLAB.png" />
<link href="phronCare.UIBlazor.styles.css" rel="stylesheet" />
<!-- Leaflet CSS -->
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.6.0/dist/leaflet.css"
@ -41,14 +42,13 @@
</div>
<div id="blazor-error-ui">
An unhandled error has occurred.
<a href="" class="reload">Reload</a>
<a class="dismiss">🗙</a>
Ocurrió un error inesperado.
<a href="" class="reload">Recargar</a>
<a class="dismiss" aria-label="Cerrar">×</a>
</div>
<!-- Blazor y JS propios -->
<script src="_framework/blazor.webassembly.js"></script>
<script src="css/fontawesome-free-6.4.2-web/js/all.min.js"></script>
<script src="js/services.js"></script>