feat(ui): refresh backoffice user experience
All checks were successful
CI/CD Pipeline / Build and Deploy with Docker Compose (pull_request) Successful in 36m19s

close #78
This commit is contained in:
Leandro Hernan Rojas 2026-07-21 09:48:54 -03:00
parent a822ae7c87
commit e8d519af9d
19 changed files with 1008 additions and 895 deletions

View File

@ -4,15 +4,21 @@
<Found Context="routeData"> <Found Context="routeData">
<AuthorizeRouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)"> <AuthorizeRouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)">
<NotAuthorized> <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> </NotAuthorized>
</AuthorizeRouteView> </AuthorizeRouteView>
<FocusOnNavigate RouteData="@routeData" Selector="h1" /> <FocusOnNavigate RouteData="@routeData" Selector="h1" />
</Found> </Found>
<NotFound> <NotFound>
<PageTitle>Not found</PageTitle> <PageTitle>Página no encontrada | PhronCare</PageTitle>
<LayoutView Layout="@typeof(MainLayout)"> <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> </LayoutView>
</NotFound> </NotFound>
</Router> </Router>

View File

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

View File

@ -1,84 +1,16 @@
.page { position: relative; display: flex; min-height: 100vh; background: var(--ph-canvas); }
.page { main { flex: 1; min-width: 0; }
position: relative; .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; }
display: flex; .sidebar-minimized { width: 57px; flex-basis: 57px; }
flex-direction: column; .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; }
main { .topbar-kicker { color: var(--ph-text-muted); font-size: .7rem; font-weight: 700; letter-spacing: .08em; text-transform: uppercase; }
flex: 1; .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, .sidebar-minimized { position: relative; width: 100%; height: auto; min-height: 4rem; }
background-color: rgb(39, 54, 97); .topbar { position: relative; min-height: 3.75rem; padding: .5rem .85rem; }
/*background-image: linear-gradient(180deg, rgb(39, 54, 97) 70%, rgb(39, 54, 97) 99%);*/ .topbar-context { display: none; }
} .topbar-actions { width: 100%; justify-content: space-between; gap: .5rem; }
.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;
}
} }

View File

@ -1,27 +1,31 @@
@inject NavMenuService navMenuService @inject NavMenuService navMenuService
<div class="top-row ps-3 navbar navbar-dark border-bottom" style="padding: 0; border:0px solid red;"> <div class="sidebar-header navbar navbar-dark @(navMenuService.Minimized ? "brand-minimized" : string.Empty)">
<div class="container-fluid" style="padding:0; border:0px solid green;"> <div class="container-fluid">
<a class="navbar-brand d-flex align-items-center" href="" style="height: 56px; border:0px solid yellow; display: block;"> <a class="navbar-brand" href="" aria-label="Ir al inicio de PhronCare">
<img src="images/SaludLAB.png" alt="" />
@if (!navMenuService.Minimized) @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> </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> <span class="navbar-toggler-icon"></span>
</button> </button>
</div> </div>
</div> </div>
<div class="@($"{NavMenuCssClass} nav-scrollable bg-dark text-white")" @onclick="ToggleNavMenu" style="padding-top:4px;"> <div class="@($"{NavMenuCssClass} nav-scrollable {(navMenuService.Minimized ? "nav-minimized" : string.Empty)}")" @onclick="ToggleNavMenu">
<nav class="flex-column text-center"> <nav class="flex-column" aria-label="Navegación principal">
<div class="nav-item px-2 py-0"> <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"> <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> <span class="oi oi-home" aria-hidden="true"></span>
@if (!navMenuService.Minimized) @if (!navMenuService.Minimized)
{ {
<label class="mb-0">Home</label> <label class="mb-0">Inicio</label>
} }
</NavLink> </NavLink>
</div> </div>
@ -47,7 +51,7 @@
</div> </div>
</AuthorizeView> </AuthorizeView>
<div class="nav-item px-2 py-0"> <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> <span class="oi oi-box" aria-hidden="true"></span>
@if (!navMenuService.Minimized) @if (!navMenuService.Minimized)
{ {
@ -55,7 +59,7 @@
} }
</NavLink> </NavLink>
@if (expStock) @if (expStock && !navMenuService.Minimized)
{ {
<ul class="nav flex-column"> <ul class="nav flex-column">
<!-- Expediciones --> <!-- Expediciones -->
@ -108,7 +112,7 @@
</div> </div>
<div class="nav-item px-2 py-0"> <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> <span class="oi oi-briefcase" aria-hidden="true"></span>
@if (!navMenuService.Minimized) @if (!navMenuService.Minimized)
{ {
@ -116,7 +120,7 @@
} }
</NavLink> </NavLink>
@if (expClientes) @if (expClientes && !navMenuService.Minimized)
{ {
<ul class="nav flex-column"> <ul class="nav flex-column">
<div class="nav-item ps-4 py-0 border-start border-2 border-white"> <div class="nav-item ps-4 py-0 border-start border-2 border-white">
@ -178,13 +182,34 @@
</NavLink> </NavLink>
</div> </div>
</nav> </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> </div>
@code { @code {
private bool collapseNavMenu = true; private bool collapseNavMenu = true;
private bool minimizeNavMenu = false; private bool expStock = false;
private bool expStock = true; private bool expClientes = false;
private bool expClientes = true;
private string? NavMenuCssClass => collapseNavMenu ? "collapse" : null; private string? NavMenuCssClass => collapseNavMenu ? "collapse" : null;
@ -199,9 +224,38 @@
collapseNavMenu = !collapseNavMenu; collapseNavMenu = !collapseNavMenu;
} }
private async void ToggleIconMenu() private async Task ToggleStockMenu()
{ {
minimizeNavMenu = !minimizeNavMenu; if (navMenuService.Minimized)
await MinStatus.InvokeAsync(minimizeNavMenu); {
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 @@
.sidebar-header { min-height: 4.25rem; padding: .55rem .75rem; background: var(--ph-sidebar-deep); border-bottom: 1px solid rgba(255, 255, 255, .08); }
.navbar-toggler { .sidebar-header .container-fluid { flex-wrap: nowrap; padding: 0; }
background-color: rgba(255, 255, 255, 0.1); .sidebar-header.brand-minimized { padding-inline: .5rem; }
} .sidebar-header.brand-minimized .container-fluid { justify-content: center; }
.icon-white { .navbar-brand { display: flex; min-width: 0; align-items: center; gap: .65rem; margin: 0; color: #fff; font-size: 1.08rem; letter-spacing: -.02em; }
color: rgb(255, 255, 255); .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; }
.top-row { .navbar-brand .brand-name { display: inline-block; white-space: nowrap; }
height: 3.5rem; .navbar-brand small { margin-top: .28rem; color: var(--ph-sidebar-muted); font-size: .58rem; font-weight: 700; letter-spacing: .13em; text-transform: uppercase; }
background-color: rgba(0,0,0,0.4); .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; }
.navbar-brand { .nav-item { padding: .12rem 0 !important; font-size: .875rem; }
font-size: 1.1rem; .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; }
.oi { .nav-item ::deep a:hover { color: #fff !important; background: rgba(255, 255, 255, .075); }
width: 2rem; .nav-item ::deep a:hover .oi, .nav-item ::deep a:hover > i { color: #fff; }
font-size: 1.1rem; .nav-item ::deep a.active { color: #fff !important; background: rgba(126, 162, 235, .2) !important; box-shadow: inset 3px 0 0 #8daef2; }
vertical-align: text-top; .nav-item ::deep a.active .oi, .nav-item ::deep a.active > i { color: #a9c2f5; }
top: -2px; .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 { .nav-item ul { gap: .05rem; margin: .2rem 0 .4rem; padding: 0; border: 0; }
font-size: 0.9rem; .nav-item ul .nav-item { padding-left: 0 !important; border: 0 !important; }
padding-bottom: 0.5rem; .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:first-of-type { .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); }
padding-top: 1rem; .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; }
.nav-item:last-of-type { .client-context span { display: flex; flex-direction: column; line-height: 1.05; }
padding-bottom: 1rem; .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; }
.nav-item ::deep a { .collapse-control i { width: 1.2rem; font-size: .9rem; }
color: #d7d7d7; .collapse-control:hover { color: #fff; background: rgba(255, 255, 255, .075); border-color: rgba(255, 255, 255, .08); }
border-radius: 4px; .collapse-control:focus-visible { outline: 3px solid rgba(141, 174, 242, .35); }
height: 3rem; @media (min-width: 768px) { .navbar-toggler { display: none; } .nav-scrollable.collapse { display: flex; } }
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; } }
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;
}
}

View File

@ -12,75 +12,121 @@
@inject ILoginService _iLoginService @inject ILoginService _iLoginService
@inject IToastService toastService @inject IToastService toastService
<div class="row" style="margin-top: 30px;"> <PageTitle>Ingresar | PhronCare · SaludLAB</PageTitle>
<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" /> <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) @if (!isSecret)
{ {
<h3>Iniciar sesión</h3> <div class="login-heading">
<EditForm Model="login2FAModel" OnInvalidSubmit="LoginAsync"> <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"> <div class="form-group">
<label>Usuario</label> <label for="username">Usuario</label>
<div> <div class="input-with-icon">
<InputText Class="form-control" @bind-Value=@login2FAModel.UserName /> <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> </div>
<div class="form-group"> <div class="form-group">
<label>Contraseña</label> <label for="password">Contraseña</label>
<div> <div class="input-with-icon">
<InputText type="password" Class="form-control" @bind-Value=login2FAModel.Password /> <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>
</div> </div>
@if (codeTwoFactor) @if (codeTwoFactor)
{ {
<div class="form-group"> <div class="form-group">
<label for="code">Código</label> <label for="code">Código de verificación</label>
<InputText type="text" id="code" class="form-control" @bind-Value=login2FAModel.Code /> <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> </div>
} }
<br /> <button type="submit" class="btn btn-primary login-submit" disabled="@isSubmitting">
<div class="col-12" style="text-align:center;"> @if (isSubmitting) { <span class="spinner-border spinner-border-sm" aria-hidden="true"></span> }
<button type="submit" class="btn btn-primary" @onclick="LoginAsync"> else { <span class="fa fa-arrow-right-to-bracket" aria-hidden="true"></span> }
<span class="fa fa-key"></span> Entrar</button> @(isSubmitting ? "Ingresando…" : "Ingresar")
<button class="btn btn-warning btn-circle" style="float:right;" @onclick="ToggleIsSecret"> </button>
<span class="fa fa-user-secret"></span> <div class="login-help">
</button> <span>¿Problemas para ingresar?</span>
<button type="button" class="reset-link" @onclick="ToggleIsSecret">Restablecer contraseña</button>
</div> </div>
</EditForm> </EditForm>
} }
else 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"> <EditForm Model="resetPassword" OnValidSubmit="ResetPasswordAsync">
<DataAnnotationsValidator /> <DataAnnotationsValidator />
<div class="form-group"> <div class="form-group">
<label>Email</label> <label for="reset-email">Email</label>
<InputText @bind-Value="resetPassword.Email" class="form-control" /> <InputText id="reset-email" type="email" autocomplete="email" @bind-Value="resetPassword.Email" class="form-control" />
<ValidationMessage For="@(() => resetPassword.Email)" />
</div> </div>
<div class="form-group"> <div class="form-group">
<label>Nueva Contraseña</label> <label for="new-password">Nueva contraseña</label>
<InputText @bind-Value="resetPassword.Password" type="password" class="form-control" /> <InputText id="new-password" @bind-Value="resetPassword.Password" type="password" autocomplete="new-password" class="form-control" />
<ValidationMessage For="@(() => resetPassword.Password)" />
</div> </div>
<div class="form-group"> <div class="form-group">
<label>Confirmar Contraseña</label> <label for="confirm-password">Confirmar contraseña</label>
<InputText @bind-Value="resetPassword.ConfirmPassword" type="password" class="form-control" /> <InputText id="confirm-password" @bind-Value="resetPassword.ConfirmPassword" type="password" autocomplete="new-password" class="form-control" />
<ValidationMessage For="@(() => resetPassword.ConfirmPassword)" />
</div> </div>
<div class="form-group"> <div class="form-group">
<label>Token</label> <label for="reset-token">Token</label>
<InputTextArea @bind-Value="resetPassword.Token" class="form-control" /> <InputTextArea id="reset-token" rows="3" @bind-Value="resetPassword.Token" class="form-control" />
<ValidationMessage For="@(() => resetPassword.Token)" />
</div> </div>
<br/> <button type="submit" class="btn btn-primary login-submit" disabled="@isSubmitting">
<div class="col-12" style="text-align:center;"> @if (isSubmitting) { <span class="spinner-border spinner-border-sm" aria-hidden="true"></span> }
<button type="submit" class="btn btn-primary"><span class="fa fa-vault"></span> Restablecer</button> else { <span class="fa fa-key" aria-hidden="true"></span> }
<button class="btn btn-warning btn-circle" style="fData.Login.Loginght;" @onclick="ToggleIsSecret"> @(isSubmitting ? "Restableciendo…" : "Restablecer contraseña")
<span class="fa fa-circle-left"></span> </button>
</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> </div>
</EditForm> </EditForm>
} }
<div class="login-legal">PhronCare by SaludLAB · Entorno BIODEC</div>
</div>
</div> </div>
</div> </section>
@code { @code {
Login loginModel = new Login(); Login loginModel = new Login();
@ -88,10 +134,15 @@
ResetPassword resetPassword = new ResetPassword(); ResetPassword resetPassword = new ResetPassword();
public bool isSecret = false; public bool isSecret = false;
bool codeTwoFactor = false; bool codeTwoFactor = false;
bool isSubmitting = false;
string OTPCode = string.Empty; string OTPCode = string.Empty;
private async Task LoginAsync() private async Task LoginAsync()
{ {
if (isSubmitting) return;
isSubmitting = true;
try
{
if (codeTwoFactor && login2FAModel.Code.Length > 4) if (codeTwoFactor && login2FAModel.Code.Length > 4)
{ {
string requestUrl = $"api/Authentication/login-2FA?code={login2FAModel.Code}&username={login2FAModel.UserName}"; string requestUrl = $"api/Authentication/login-2FA?code={login2FAModel.Code}&username={login2FAModel.UserName}";
@ -136,9 +187,18 @@
toastService.ShowError(tokenResponse); toastService.ShowError(tokenResponse);
} }
} }
}
finally
{
isSubmitting = false;
}
} }
private async Task ResetPasswordAsync() private async Task ResetPasswordAsync()
{ {
if (isSubmitting) return;
isSubmitting = true;
try
{
if (resetPassword.Password != resetPassword.ConfirmPassword) if (resetPassword.Password != resetPassword.ConfirmPassword)
{ {
toastService.ShowError("Las contraseñas no coinciden. ¡Inténtalo de nuevo!"); toastService.ShowError("Las contraseñas no coinciden. ¡Inténtalo de nuevo!");
@ -151,6 +211,11 @@
{ {
await ResetingPassword(); await ResetingPassword();
} }
}
finally
{
isSubmitting = false;
}
} }
private async Task ResetingPassword() private async Task ResetingPassword()
{ {

View File

@ -1,9 +1,43 @@
.btn-circle { .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); }
width: 40px; .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%); }
height: 40px; .login-showcase::before, .login-showcase::after { position: absolute; content: ""; border: 1px solid rgba(255,255,255,.08); border-radius: 50%; }
padding: 6px 0px; .login-showcase::before { right: -12rem; bottom: -12rem; width: 30rem; height: 30rem; }
border-radius: 50px; .login-showcase::after { right: -5rem; bottom: -5rem; width: 18rem; height: 18rem; }
text-align: center; .showcase-brand { position: relative; z-index: 1; display: flex; align-items: center; gap: .75rem; font-size: 1.35rem; }
font-size: 20px; .showcase-brand img { width: 46px; height: 46px; object-fit: contain; border-radius: .75rem; }
line-height: 1.42857; .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 @using phronCare.UIBlazor.Pages.Authorization
@inject NavigationManager Navigation @inject NavigationManager Navigation
<PageTitle>PhronCare</PageTitle> <PageTitle>PhronCare · SaludLAB</PageTitle>
<AuthorizeView> <AuthorizeView>
<Authorized> <Authorized>
<section class="home-dashboard"> <section class="home-dashboard" aria-label="Panel principal">
<div class="home-hero"> <header class="home-intro">
<div> <div>
<span class="home-eyebrow">BackOffice operativo</span> <span class="section-eyebrow">Centro operativo</span>
<h1>Bienvenido a PhronCare</h1> <h1>Panel principal</h1>
<p> <p>Gestión asistencial, comercial y logística de PhronCare.</p>
Gestion integral para empresas de internacion domiciliaria,
ortopedia y prestaciones medicas.
</p>
</div> </div>
<div class="home-hero-status" aria-label="Estado general"> <div class="platform-status" aria-label="Estado general: plataforma operativa">
<span class="home-status-dot"></span> <span class="status-dot"></span>
Plataforma operativa Plataforma operativa
</div> </div>
</div> </header>
<div class="home-section-header"> <section class="dashboard-section" aria-labelledby="daily-title">
<h2>Accesos rapidos</h2> <div class="section-heading">
<span>Modulos principales</span> <div>
</div> <span class="section-eyebrow">Accesos principales</span>
<h2 id="daily-title">Operación diaria</h2>
<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>
</div> </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) @foreach (var item in SummaryItems)
{ {
<div class="kpi-card"> <article class="metric-item">
<div class="kpi-icon @item.AccentClass"> <span class="metric-icon @item.AccentClass"><i class="@item.Icon" aria-hidden="true"></i></span>
<i class="@item.Icon" aria-hidden="true"></i> <span class="metric-copy">
</div>
<div>
<strong>@item.Value</strong> <strong>@item.Value</strong>
<span>@item.Title</span> <small>@item.Title</small>
</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>
</span> </span>
<div> </article>
<strong>@item.Title</strong>
<small>@item.Description</small>
</div>
</div>
} }
</div> </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> </section>
</Authorized> </Authorized>
<NotAuthorized> <NotAuthorized>
@ -97,23 +125,27 @@
@code @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", "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("Presupuestos", "Consulta y seguimiento de presupuestos.", "/quotes/dashboard", "fas fa-file-invoice-dollar", "accent-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")
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"), private readonly List<ManagementAccessItem> ManagementItems =
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("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 = private readonly List<SummaryItem> SummaryItems =
[ [
new("Presupuestos Activos", "128", "fas fa-file-signature", "accent-green"), new("Presupuestos activos", "128", "fas fa-file-signature", "accent-green"),
new("Remitos Emitidos", "42", "fas fa-truck-fast", "accent-orange"), new("Remitos emitidos", "42", "fas fa-truck-fast", "accent-orange"),
new("Facturas Pendientes", "15", "fas fa-file-invoice", "accent-teal"), new("Facturas pendientes", "15", "fas fa-file-invoice", "accent-teal"),
new("Tickets Abiertos", "6", "fas fa-headset", "accent-red") new("Tickets abiertos", "6", "fas fa-headset", "accent-red")
]; ];
private readonly List<ActivityItem> RecentActivity = private readonly List<ActivityItem> RecentActivity =
@ -124,12 +156,10 @@
new("Ticket cerrado", "Hoy, 09:20", "fas fa-circle-check", "accent-red") new("Ticket cerrado", "Hoy, 09:20", "fas fa-circle-check", "accent-red")
]; ];
private void NavigateTo(string url) private void NavigateTo(string url) => Navigation.NavigateTo(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 SummaryItem(string Title, string Value, string Icon, string AccentClass);
private sealed record ActivityItem(string Title, string Description, string Icon, string AccentClass); private sealed record ActivityItem(string Title, string Description, string Icon, string AccentClass);
} }

View File

@ -1,318 +1,466 @@
.home-dashboard { .home-dashboard {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 1.5rem; gap: clamp(2.2rem, 4vw, 3.75rem);
padding-bottom: 2rem; padding: .25rem 0 3rem;
} }
.home-hero { .home-intro,
align-items: flex-start; .section-heading {
background: var(--background-highlight);
border: 1px solid var(--background-highlight-light);
border-radius: 8px;
display: flex; display: flex;
gap: 1rem; align-items: flex-end;
justify-content: space-between; justify-content: space-between;
padding: 1.75rem; gap: 2rem;
} }
.home-eyebrow { .home-intro h1 {
color: var(--link-color); 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; display: block;
font-size: 0.8rem; color: var(--ph-primary);
font-weight: 700; font-size: .72rem;
letter-spacing: 0; font-weight: 750;
margin-bottom: 0.45rem; letter-spacing: .1em;
text-transform: uppercase; text-transform: uppercase;
} }
.home-hero h1 { .platform-status {
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);
display: inline-flex; display: inline-flex;
align-items: center;
flex: 0 0 auto; flex: 0 0 auto;
font-size: 0.85rem; gap: .55rem;
font-weight: 600; padding: .48rem .75rem;
gap: 0.5rem; color: var(--ph-text-muted);
padding: 0.45rem 0.75rem; background: var(--ph-surface);
white-space: nowrap; border: 1px solid var(--ph-border);
}
.home-status-dot {
background: #16a085;
border-radius: 999px; border-radius: 999px;
box-shadow: 0 0 0 4px rgba(22, 160, 133, 0.14); font-size: .78rem;
height: 0.6rem; font-weight: 650;
width: 0.6rem; box-shadow: var(--ph-shadow-sm);
} }
.home-section-header { .status-dot {
align-items: baseline; width: .52rem;
display: flex; height: .52rem;
gap: 0.75rem; background: #35a58a;
justify-content: space-between; border-radius: 50%;
box-shadow: 0 0 0 4px color-mix(in srgb, #35a58a 14%, transparent);
} }
.home-section-header h2 { .dashboard-section {
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 {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 0.2rem; gap: 1.15rem;
min-width: 0;
} }
.quick-access-content strong { .section-heading h2 {
color: var(--text-background); margin: .25rem 0 0;
font-size: 0.95rem; color: var(--ph-text);
line-height: 1.2; font-size: clamp(1.25rem, 2vw, 1.55rem);
font-weight: 730;
} }
.quick-access-content small { .section-heading > p {
color: var(--text-background); max-width: 38rem;
font-size: 0.78rem; margin: 0 0 .12rem;
line-height: 1.35; font-size: .9rem;
opacity: 0.66; line-height: 1.45;
text-align: right;
} }
.quick-access-arrow { .featured-grid {
color: var(--text-background);
font-size: 0.8rem;
opacity: 0.48;
}
.home-content-grid {
display: 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)); grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 1rem;
} }
.kpi-card, .featured-card,
.activity-list { .management-card {
background: var(--background-highlight); position: relative;
border: 1px solid var(--background-highlight-light); color: var(--ph-text);
border-radius: 8px; cursor: pointer;
text-align: left;
font: inherit;
} }
.kpi-card { .featured-card {
align-items: center;
display: flex; display: flex;
gap: 0.9rem; min-height: 18rem;
min-height: 92px; flex-direction: column;
padding: 1rem; 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 { .featured-card::after {
font-size: 1rem; position: absolute;
height: 2.65rem; right: -5rem;
width: 2.65rem; 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 { .featured-card:hover,
color: var(--text-background); .featured-card:focus-visible {
display: block; border-color: var(--feature-color);
font-size: 1.75rem; 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; line-height: 1;
} }
.kpi-card span { .metric-copy small {
color: var(--text-background); color: var(--ph-text-muted);
display: block; font-size: .78rem;
font-size: 0.85rem;
margin-top: 0.35rem;
opacity: 0.68;
} }
.activity-list { .activity-panel {
display: flex;
flex-direction: column;
overflow: hidden; 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 { .activity-item {
align-items: center;
display: flex; display: flex;
gap: 0.85rem; min-height: 4.2rem;
min-height: 72px; align-items: center;
padding: 0.95rem 1rem; gap: .75rem;
padding: .75rem 1.2rem;
} }
.activity-item + .activity-item { .activity-item + .activity-item {
border-top: 1px solid var(--background-highlight-light); border-top: 1px solid var(--ph-border);
} }
.activity-icon { .activity-icon {
font-size: 0.9rem; width: 2.1rem;
height: 2.25rem; height: 2.1rem;
width: 2.25rem; border-radius: .55rem;
font-size: .75rem;
}
.activity-item strong,
.activity-item small {
display: block;
} }
.activity-item strong { .activity-item strong {
color: var(--text-background); color: var(--ph-text);
display: block; font-size: .8rem;
font-size: 0.92rem;
line-height: 1.25;
} }
.activity-item small { .activity-item small {
color: var(--text-background); margin-top: .15rem;
display: block; color: var(--ph-text-muted);
font-size: 0.78rem; font-size: .7rem;
margin-top: 0.18rem;
opacity: 0.62;
} }
.accent-blue { .accent-blue { --accent-color: #5288c7; }
background: #2473a6; .accent-green { --accent-color: #49a38b; }
} .accent-indigo { --accent-color: #7186ab; }
.accent-orange { --accent-color: #d59a34; }
.accent-green { .accent-teal { --accent-color: #55a0b0; }
background: #138f77; .accent-violet { --accent-color: #8d62b5; }
} .accent-red { --accent-color: #cc604e; }
.accent-indigo {
background: #34495e;
}
.accent-orange {
background: #da8c10;
}
.accent-teal {
background: #2c8f9e;
}
.accent-violet {
background: #7f3d9b;
}
.accent-red {
background: #cf4435;
}
@media (max-width: 1199.98px) { @media (max-width: 1199.98px) {
.quick-access-grid { .management-grid { grid-template-columns: repeat(3, minmax(0, 1fr)); }
grid-template-columns: repeat(3, minmax(0, 1fr));
}
} }
@media (max-width: 991.98px) { @media (max-width: 991.98px) {
.home-content-grid { .overview-grid { grid-template-columns: 1fr; }
grid-template-columns: 1fr;
}
.quick-access-grid {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
} }
@media (max-width: 767.98px) { @media (max-width: 767.98px) {
.home-hero { .home-dashboard { gap: 2.5rem; }
flex-direction: column; .home-intro,
padding: 1.25rem; .section-heading { align-items: flex-start; flex-direction: column; gap: .8rem; }
} .section-heading > p { max-width: none; text-align: left; }
.featured-grid,
.home-hero h1 { .management-grid { grid-template-columns: 1fr; }
font-size: 1.6rem; .featured-card { min-height: 15rem; }
} .management-card { min-height: 8.5rem; }
}
.home-hero-status {
white-space: normal; @media (max-width: 479.98px) {
} .platform-status { align-self: flex-start; }
.metrics-panel { grid-template-columns: 1fr; }
.kpi-grid, .metric-item:nth-child(even) { border-left: 0; }
.quick-access-grid { .metric-item:nth-child(n+2) { border-top: 1px solid var(--ph-border); }
grid-template-columns: 1fr; .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 @using System.Security.Claims
<AuthorizeView> <AuthorizeView>
<Authorized Context="authContext"> <Authorized Context="authContext">
<div class="d-flex justify-content-end align-items-center gap-3 px-3 py-2"> <div class="login-state">
<span class="text-dark"> <span class="user-avatar" aria-hidden="true">@GetInitial(authContext.User)</span>
Bienvenido, @GetUserDisplayName(authContext.User) <span class="user-copy">
<small>Sesión activa</small>
<strong>@GetUserDisplayName(authContext.User)</strong>
</span> </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> <i class="oi oi-account-logout"></i>
<span>Cerrar sesión</span>
</a> </a>
</div> </div>
</Authorized> </Authorized>
@ -19,4 +20,9 @@
?? user.Identity?.Name ?? user.Identity?.Name
?? "Usuario"; ?? "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 { .login-state { display: flex; min-width: 0; align-items: center; gap: .65rem; }
text-decoration: none !important; .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; }
margin-left: 20px; .user-copy { display: flex; min-width: 0; flex-direction: column; line-height: 1.15; text-align: left; }
padding: 8px 8px 8px 8px; .user-copy small { color: var(--ph-text-muted); font-size: .66rem; }
background-image: linear-gradient(90deg, rgb(169, 15, 144) 0%, rgb(23, 25, 85) 90%); .user-copy strong { max-width: 13rem; overflow: hidden; color: var(--ph-text); font-size: .82rem; text-overflow: ellipsis; white-space: nowrap; }
border-radius: 4px 12px 12px 12px; .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; }
color: white; .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)); }
cursor: pointer; @media (max-width: 575.98px) { .user-copy { display: none; } }
} */
.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;
}

View File

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

View File

@ -1,16 +1,16 @@
ph-header { .ph-table-toolbar { display: flex; align-items: center; justify-content: space-between; gap: 1rem; margin-bottom: .85rem; }
color: whitesmoke; .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-container { .ph-search-input ::deep input { padding-left: 2.45rem; }
overflow: auto; .ph-pagination-area { margin-left: auto; }
height: 460px; .ph-header.sortable { cursor: pointer; user-select: none; }
} .ph-header.sortable:hover { color: var(--ph-primary); background: var(--ph-primary-soft); }
.ph-container thead th { .actions-header { cursor: default; user-select: none; }
position:sticky; .ph-actions { display: flex; flex-wrap: wrap; gap: .4rem; min-width: 7rem; }
top:0; .ph-actions ::deep .btn { margin: 0 !important; }
z-index:1; .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; }
td, th { .ph-empty-state strong { color: var(--ph-text); }
padding: 5px .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"> <div class="toggle-container">
<label class="switch"> <label class="switch" title="Cambiar tema visual">
<input type="checkbox" class="switch" @bind=ToggleState /> <input type="checkbox" class="switch" aria-label="Activar tema oscuro" @bind=ToggleState />
<span class="slider round"></span> <span class="slider round"></span>
</label> </label>
@if (!string.IsNullOrEmpty(Caption)) @if (!string.IsNullOrEmpty(Caption))
{ {
<span class="toggle-label">@Caption</span> <span class="toggle-label">@Caption</span>
} }
<label style="margin:10px;"> <span class="toggle-icon">
@ChildContent @ChildContent
</label> </span>
</div> </div>
@code { @code {
[Parameter] [Parameter]

View File

@ -1,74 +1,11 @@
/* Begin Toggle Switch*/ .toggle-container { display: flex; align-items: center; gap: .5rem; color: var(--ph-text-muted); }
.toggle-container { .switch { position: relative; display: inline-block; width: 2.45rem; height: 1.4rem; margin: 0; }
display: flex; .switch input { width: 0; height: 0; opacity: 0; }
align-items: center; .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; }
.toggle-label { input:checked + .slider { background: #3157a4; }
margin-left: 15px; /* Ajusta el espaciado según sea necesario */ input:focus-visible + .slider { box-shadow: 0 0 0 .22rem var(--ph-focus); }
font-size: 130%; input:checked + .slider::before { transform: translateX(1.05rem); }
} .toggle-label { color: var(--ph-text-muted); font-size: .76rem; font-weight: 600; white-space: nowrap; }
/* The switch - the box around the slider */ .toggle-icon { display: inline-flex; width: 1rem; justify-content: center; font-size: .85rem; }
.switch { @media (max-width: 575.98px) { .toggle-label { display: none; } }
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*/

View File

@ -1,181 +1,128 @@
@import url('open-iconic/font/css/open-iconic-bootstrap.min.css'); @import url('open-iconic/font/css/open-iconic-bootstrap.min.css');
:root { :root {
--background: white; color-scheme: light;
--text-background: black; --ph-primary: #3157a4; --ph-primary-hover: #274887; --ph-primary-soft: #e9effb;
--background-highlight: #f7f7f7; --ph-accent: #159b8c; --ph-danger: #c33d4a; --ph-warning: #b86f0b; --ph-success: #18785d; --ph-info: #2476a8;
--background-highlight-light: #e2e3e5; --ph-canvas: #f4f6fa; --ph-surface: #fff; --ph-surface-subtle: #f8fafc; --ph-surface-raised: #fff;
--link-color: #0366d6; --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 { .dark-mode {
--background: #1C1B1F; color-scheme: dark;
--text-background: white; --ph-primary: #8daef2; --ph-primary-hover: #abc2f3; --ph-primary-soft: #263a63;
--background-highlight: #202124; --ph-accent: #52c7b8; --ph-danger: #f27a84; --ph-warning: #f1b85c; --ph-success: #63c5a5; --ph-info: #74b9e4;
--background-highlight-light: #3C4043; --ph-canvas: #101725; --ph-surface: #182131; --ph-surface-subtle: #1d283a; --ph-surface-raised: #202c40;
--link-color: #3ca4ff; --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;
.nav-flex-column { --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);
list-style-type: square;
padding-left: 20px; /* Alineación adecuada para submenús */
} }
.nav-item { *, *::before, *::after { box-sizing: border-box; }
padding-top: 0.5rem; 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; }
padding-bottom: 0.5rem; 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 { .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); }
font-size: 0.875rem; .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 { .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; }
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; .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 { .form-label, label { margin-bottom: .42rem; color: var(--ph-text); font-size: .875rem; font-weight: 600; }
outline: none; .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 { .table-responsive, .ph-table-scroll { overflow-x: auto; border: 1px solid var(--ph-border); border-radius: var(--ph-radius); }
background-color: rgba(255, 255, 255, 0.1); .table { margin-bottom: 0; color: var(--ph-text); background: var(--ph-surface); border-color: var(--ph-border); vertical-align: middle; }
color: #fff; .table > :not(caption) > * > * { padding: .78rem .85rem; color: inherit; background: transparent; border-bottom-color: var(--ph-border); box-shadow: none; }
transition: background-color 0.2s; .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 { .badge { padding: .42em .65em; border-radius: 999px; font-weight: 650; }
margin-bottom: 0.25rem; .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); }
a, .btn-link { .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; }
/*color: #0071c1;*/ .modal-header, .modal-footer { border-color: var(--ph-border); }
color: var(--link-color); .modal-body { padding: 1.35rem; }
} .modal-footer { gap: .5rem; padding: 1rem 1.35rem; background: var(--ph-surface-subtle); }
.alert-secondary { .blazored-modal-overlay { background-color: rgba(8, 15, 30, .62) !important; backdrop-filter: blur(2px); }
background-color: var(--background-highlight-light); .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; }
color: var(--text-background); .blazored-toast-heading { color: var(--ph-text) !important; }
} .blazored-toast-message { color: var(--ph-text-muted) !important; }
.table { .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); }
color: var(--text-background); .spinner-border { color: var(--ph-primary); }
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);
}
.card-body { #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); }
background-color: var(--background-highlight); #blazor-error-ui a { color: #fff; }
color: var(--text-background); #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 { @media (prefers-reduced-motion: reduce) { *, *::before, *::after { scroll-behavior: auto !important; transition-duration: .01ms !important; animation-duration: .01ms !important; } }
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 */
}

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"> <html lang="es">
<head> <head>
<meta charset="utf-8" /> <meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" /> <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="/" /> <base href="/" />
<!-- Estilos propios --> <!-- Estilos propios -->
<link href="css/bootstrap/bootstrap.min.css" rel="stylesheet" /> <link href="css/bootstrap/bootstrap.min.css" rel="stylesheet" />
<link href="css/app.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 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" /> <link href="phronCare.UIBlazor.styles.css" rel="stylesheet" />
<!-- Leaflet CSS --> <!-- Leaflet CSS -->
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.6.0/dist/leaflet.css" <link rel="stylesheet" href="https://unpkg.com/leaflet@1.6.0/dist/leaflet.css"
@ -41,14 +42,13 @@
</div> </div>
<div id="blazor-error-ui"> <div id="blazor-error-ui">
An unhandled error has occurred. Ocurrió un error inesperado.
<a href="" class="reload">Reload</a> <a href="" class="reload">Recargar</a>
<a class="dismiss">🗙</a> <a class="dismiss" aria-label="Cerrar">×</a>
</div> </div>
<!-- Blazor y JS propios --> <!-- Blazor y JS propios -->
<script src="_framework/blazor.webassembly.js"></script> <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> <script src="js/services.js"></script>