Compare commits

..

5 Commits

Author SHA1 Message Date
61e55e014f trabajando en el home 2026-02-04 20:36:08 +01:00
48993a34c4 Merge branch 'feat/impersonate' into 'main'
impersonation implementado

See merge request jjimenez/erp-imprimelibros!34
2026-02-04 18:05:44 +00:00
a0bf8552f1 impersonation implementado 2026-02-04 19:05:10 +01:00
562dc2b231 Merge branch 'feat/log_save_presupuesto' into 'main'
añadido log en guardar presupuesto

See merge request jjimenez/erp-imprimelibros!33
2026-01-09 16:55:09 +00:00
9a49ccf6b8 añadido log en guardar presupuesto 2026-01-09 17:54:06 +01:00
16 changed files with 2745 additions and 12014 deletions

14036
logs/erp.log

File diff suppressed because one or more lines are too long

View File

@ -149,6 +149,10 @@ public class SecurityConfig {
"/pagos/redsys/**" "/pagos/redsys/**"
) )
.permitAll() .permitAll()
.requestMatchers("/impersonate/exit")
.hasRole("PREVIOUS_ADMINISTRATOR")
.requestMatchers("/impersonate")
.hasAnyRole("SUPERADMIN", "ADMIN")
.requestMatchers("/users/**").hasAnyRole("SUPERADMIN", "ADMIN") .requestMatchers("/users/**").hasAnyRole("SUPERADMIN", "ADMIN")
.anyRequest().authenticated()) .anyRequest().authenticated())

View File

@ -14,6 +14,8 @@ import java.util.Optional;
import java.util.List; import java.util.List;
import java.util.Set; import java.util.Set;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.MessageSource; import org.springframework.context.MessageSource;
import org.springframework.http.ResponseEntity; import org.springframework.http.ResponseEntity;
@ -63,6 +65,8 @@ import jakarta.validation.Valid;
@RequestMapping("/presupuesto") @RequestMapping("/presupuesto")
public class PresupuestoController { public class PresupuestoController {
private static final Logger log = LoggerFactory.getLogger(PresupuestoController.class);
private final PresupuestoRepository presupuestoRepository; private final PresupuestoRepository presupuestoRepository;
@Autowired @Autowired
@ -824,6 +828,7 @@ public class PresupuestoController {
return ResponseEntity.ok(Map.of("id", saveResult.get("presupuesto_id"), return ResponseEntity.ok(Map.of("id", saveResult.get("presupuesto_id"),
"message", messageSource.getMessage("presupuesto.exito.guardado", null, locale))); "message", messageSource.getMessage("presupuesto.exito.guardado", null, locale)));
} catch (Exception ex) { } catch (Exception ex) {
log.error("Error al guardar el presupuesto", ex);
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body(Map.of("message", .body(Map.of("message",
messageSource.getMessage("presupuesto.error.save-internal-error", null, locale), messageSource.getMessage("presupuesto.error.save-internal-error", null, locale),

View File

@ -0,0 +1,115 @@
package com.imprimelibros.erp.users;
import java.util.ArrayList;
import java.util.List;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import com.imprimelibros.erp.config.Sanitizer;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpSession;
@Controller
public class ImpersonationController {
private static final String PREVIOUS_ADMIN_ROLE = "ROLE_PREVIOUS_ADMINISTRATOR";
private static final String SESSION_ATTR = "IMPERSONATOR_AUTH";
private final UserService userService;
private final Sanitizer sanitizer;
public ImpersonationController(UserService userService, Sanitizer sanitizer) {
this.userService = userService;
this.sanitizer = sanitizer;
}
@PostMapping("/impersonate")
@PreAuthorize("hasRole('ADMIN') or hasRole('SUPERADMIN')")
public String impersonate(
@RequestParam("username") String username,
Authentication authentication,
HttpServletRequest request) {
if (authentication == null) {
return "redirect:/login";
}
if (hasRole(authentication, PREVIOUS_ADMIN_ROLE)) {
return "redirect:/";
}
String normalized = sanitizer.plain(username);
if (normalized == null || normalized.isBlank()) {
return "redirect:/users";
}
normalized = normalized.trim().toLowerCase();
if (authentication.getName() != null
&& authentication.getName().equalsIgnoreCase(normalized)) {
return "redirect:/users";
}
UserDetails target;
try {
target = userService.loadUserByUsername(normalized);
} catch (UsernameNotFoundException ex) {
throw new AccessDeniedException("No autorizado");
}
boolean currentIsSuperAdmin = hasRole(authentication, "ROLE_SUPERADMIN");
boolean targetIsSuperAdmin = target.getAuthorities().stream()
.anyMatch(a -> "ROLE_SUPERADMIN".equals(a.getAuthority()));
if (targetIsSuperAdmin && !currentIsSuperAdmin) {
throw new AccessDeniedException("No autorizado");
}
HttpSession session = request.getSession(true);
if (session.getAttribute(SESSION_ATTR) == null) {
session.setAttribute(SESSION_ATTR, authentication);
}
List<GrantedAuthority> authorities = new ArrayList<>(target.getAuthorities());
authorities.add(new SimpleGrantedAuthority(PREVIOUS_ADMIN_ROLE));
UsernamePasswordAuthenticationToken newAuth = new UsernamePasswordAuthenticationToken(
target, target.getPassword(), authorities);
newAuth.setDetails(authentication.getDetails());
SecurityContextHolder.getContext().setAuthentication(newAuth);
return "redirect:/";
}
@PostMapping("/impersonate/exit")
@PreAuthorize("hasRole('PREVIOUS_ADMINISTRATOR')")
public String exit(HttpServletRequest request) {
HttpSession session = request.getSession(false);
if (session != null) {
Object previous = session.getAttribute(SESSION_ATTR);
if (previous instanceof Authentication previousAuth) {
SecurityContextHolder.getContext().setAuthentication(previousAuth);
} else {
SecurityContextHolder.clearContext();
}
session.removeAttribute(SESSION_ATTR);
}
return "redirect:/";
}
private static boolean hasRole(Authentication auth, String role) {
return auth != null
&& auth.getAuthorities().stream()
.anyMatch(a -> role.equals(a.getAuthority()));
}
}

View File

@ -81,6 +81,9 @@ public class UserController {
"usuarios.delete.button", "usuarios.delete.button",
"app.yes", "app.yes",
"app.cancelar", "app.cancelar",
"usuarios.impersonate.title",
"usuarios.impersonate.text",
"usuarios.impersonate.button",
"usuarios.delete.ok.title", "usuarios.delete.ok.title",
"usuarios.delete.ok.text"); "usuarios.delete.ok.text");
@ -132,26 +135,36 @@ public class UserController {
.collect(Collectors.joining(" "))) .collect(Collectors.joining(" ")))
.add("actions", (user) -> { .add("actions", (user) -> {
boolean isSuperAdmin = authentication.getAuthorities().stream() boolean isSuperAdmin = authentication != null && authentication.getAuthorities().stream()
.anyMatch(a -> a.getAuthority().equals("ROLE_SUPERADMIN")); .anyMatch(a -> a.getAuthority().equals("ROLE_SUPERADMIN"));
if (!isSuperAdmin) { boolean isSelf = authentication != null
return "<div class=\"hstack gap-3 flex-wrap\">\n" + && authentication.getName() != null
" <a href=\"javascript:void(0);\" data-id=\"" + user.getId() && authentication.getName().equalsIgnoreCase(user.getUserName());
+ "\" class=\"link-success btn-edit-user fs-15\"><i class=\"ri-edit-2-line\"></i></a>\n"
+ boolean targetIsSuperAdmin = user.getRoles().stream()
" </div>"; .anyMatch(r -> "SUPERADMIN".equalsIgnoreCase(r.getName()));
} else {
// Admin editando otro admin o usuario normal: puede editarse y eliminarse StringBuilder actions = new StringBuilder();
return "<div class=\"hstack gap-3 flex-wrap\">\n" + actions.append("<div class=\"hstack gap-3 flex-wrap\">");
" <a href=\"javascript:void(0);\" data-id=\"" + user.getId() actions.append("<a href=\"javascript:void(0);\" data-id=\"")
+ "\" class=\"link-success btn-edit-user fs-15\"><i class=\"ri-edit-2-line\"></i></a>\n" .append(user.getId())
+ .append("\" class=\"link-success btn-edit-user fs-15\"><i class=\"ri-edit-2-line\"></i></a>");
" <a href=\"javascript:void(0);\" data-id=\"" + user.getId()
+ "\" class=\"link-danger btn-delete-user fs-15\"><i class=\"user-delete ri-delete-bin-line\"></i></a>\n" if (!isSelf && (isSuperAdmin || !targetIsSuperAdmin)) {
+ actions.append("<a href=\"javascript:void(0);\" data-username=\"")
" </div>"; .append(user.getUserName())
.append("\" class=\"link-info btn-impersonate-user fs-15\"><i class=\"ri-user-shared-line\"></i></a>");
} }
if (isSuperAdmin) {
actions.append("<a href=\"javascript:void(0);\" data-id=\"")
.append(user.getId())
.append("\" class=\"link-danger btn-delete-user fs-15\"><i class=\"user-delete ri-delete-bin-line\"></i></a>");
}
actions.append("</div>");
return actions.toString();
}) })
.where(base) .where(base)
// Filtros custom: // Filtros custom:

View File

@ -1,7 +1,7 @@
spring.application.name=erp spring.application.name=erp
# Active profile # Active profile
#spring.profiles.active=dev spring.profiles.active=dev
spring.profiles.active=test #spring.profiles.active=test
#spring.profiles.active=prod #spring.profiles.active=prod

View File

@ -6,4 +6,5 @@ app.cancelar=Cancel
app.guardar=Save app.guardar=Save
app.editar=Edit app.editar=Edit
app.eliminar=Delete app.eliminar=Delete
app.imprimir=Print app.imprimir=Print
app.impersonate.exit=Return to my user

View File

@ -32,4 +32,5 @@ app.sidebar.gestion-pagos=Gestión de Pagos
app.errors.403=No tienes permiso para acceder a esta página. app.errors.403=No tienes permiso para acceder a esta página.
app.validation.required=Campo obligatorio app.validation.required=Campo obligatorio
app.impersonate.exit=Volver a mi usuario

View File

@ -1 +1,3 @@
usuarios.impersonate.title=Sign in as user
usuarios.impersonate.text=You are about to sign in as <b>{0}</b>. You can return to your user from the menu.
usuarios.impersonate.button=Continue

View File

@ -53,4 +53,7 @@ usuarios.delete.title=Eliminar usuario
usuarios.delete.button=Si, ELIMINAR usuarios.delete.button=Si, ELIMINAR
usuarios.delete.text=¿Está seguro de que desea eliminar al usuario?<br>Esta acción no se puede deshacer. usuarios.delete.text=¿Está seguro de que desea eliminar al usuario?<br>Esta acción no se puede deshacer.
usuarios.delete.ok.title=Usuario eliminado usuarios.delete.ok.title=Usuario eliminado
usuarios.delete.ok.text=El usuario ha sido eliminado con éxito. usuarios.delete.ok.text=El usuario ha sido eliminado con éxito.
usuarios.impersonate.title=Entrar como usuario
usuarios.impersonate.text=Vas a iniciar sesión como <b>{0}</b>. Podrás volver a tu usuario desde el menú.
usuarios.impersonate.button=Entrar

View File

@ -0,0 +1,312 @@
:root {
/* ====== Colores (cámbialos a tu gusto) ====== */
--banner-bg-1: #a5a091;
--banner-bg-2: #8292a8;
--banner-panel-bg: #a1b1b2;
--banner-panel-border: rgba(255, 255, 255, .75);
--text-main: #ffffff;
--text-muted: rgba(255, 255, 255, .8);
--accent-1: #e5745b;
/* salmón */
--accent-2: #92b2a7;
/* tu verde corporativo */
--accent-3: #7cc7ff;
/* toque azul claro */
--card-bg: #ffffff;
--card-border: rgba(8, 42, 67, .18);
--card-title: #0a314b;
--card-chip-bg: var(--accent-1);
--card-chip-text: #ffffff;
--shadow: 0 10px 30px rgba(0, 0, 0, .18);
/* ====== Medidas ====== */
--radius-lg: 18px;
--radius-md: 14px;
--radius-sm: 10px;
--pad: 22px;
}
.ib-loyalty-banner {
position: relative;
width: 100%;
overflow: hidden;
border-radius: var(--radius-lg);
box-shadow: var(--shadow);
color: var(--text-main);
/* padding fluido */
padding: clamp(14px, 2.2vw, 22px);
/* Importante: reserva espacio inferior para decoraciones (libro) en desktop */
padding-bottom: clamp(18px, 3.2vw, 46px);
/* Fondo con gradiente + textura sutil */
background:
radial-gradient(1200px 500px at 20% 0%, rgba(124, 199, 255, .20), transparent 60%),
radial-gradient(900px 450px at 90% 20%, rgba(243, 162, 133, .22), transparent 65%),
linear-gradient(135deg, var(--banner-bg-1), var(--banner-bg-2));
}
/* Opcional pero ayuda a que haya “lienzo” para el libro */
@media (min-width: 1101px) {
.ib-loyalty-banner {
min-height: 240px;
}
}
/* ===== Decoraciones generales ===== */
.ib-loyalty-banner .decor {
position: absolute;
inset: 0;
pointer-events: none;
opacity: .95;
}
/* Círculos/“sellos” */
.ib-loyalty-banner .decor::before,
.ib-loyalty-banner .decor::after {
content: "";
position: absolute;
width: 140px;
height: 140px;
border-radius: 50%;
border: 10px solid rgba(243, 162, 133, .65);
box-shadow: inset 0 0 0 10px rgba(255, 255, 255, .08);
}
.ib-loyalty-banner .decor::before {
top: -42px;
right: -50px;
transform: rotate(10deg);
}
.ib-loyalty-banner .decor::after {
bottom: -55px;
left: -55px;
border-color: rgba(243, 162, 133, .55);
}
/* ===== Libros “dibujados” con SVG como background ===== */
.ib-loyalty-banner .book {
position: absolute;
width: 190px;
height: 150px;
opacity: .9;
background-repeat: no-repeat;
background-position: center;
background-size: contain;
background-image: url("/assets/images/open-book.svg");
/* Sombra sin cargarte otros filtros */
filter: drop-shadow(0 10px 18px rgba(0, 0, 0, .25));
}
/* Libro pequeño: lo subimos un poco para que no se quede “bajo” cuando el layout crece */
.ib-loyalty-banner .book.small {
width: 150px;
height: 120px;
left: 22px;
bottom: 18px;
/* antes 10px */
opacity: .85;
z-index: 1;
/* por encima del fondo, por debajo del contenido (contenido z-index 2) */
}
/* ===== Contenido ===== */
.ib-loyalty-inner {
position: relative;
display: grid;
gap: clamp(12px, 2vw, 18px);
/* CLAVE: NO estires ambas columnas a la misma altura */
align-items: start;
z-index: 2;
/* Dos columnas con mínimos reales */
grid-template-columns: minmax(320px, 1.05fr) minmax(320px, .95fr);
}
/* Apila antes para que no se estrangule */
@media (max-width: 1100px) {
.ib-loyalty-inner {
grid-template-columns: 1fr;
}
}
/* Panel principal (logo + textos) */
.ib-loyalty-hero {
border: 2px solid var(--banner-panel-border);
border-radius: var(--radius-lg);
background: var(--banner-panel-bg);
padding: 18px;
position: relative;
}
/*.ib-loyalty-hero::after {
content: "";
position: absolute;
left: 0;
right: 0;
bottom: -2px;
height: 6px;
background: linear-gradient(90deg, transparent, var(--accent-1), transparent);
opacity: .9;
border-radius: 999px;
}*/
.ib-loyalty-head {
display: flex;
gap: 14px;
align-items: flex-start;
flex-wrap: wrap;
/* responsive */
}
.ib-loyalty-logo {
width: 56px;
height: 56px;
border-radius: 14px;
display: flex;
align-items: center;
justify-content: center;
flex: 0 0 auto;
}
.ib-loyalty-logo img {
width: 38px;
height: 38px;
object-fit: contain;
}
.ib-loyalty-title {
margin: 0;
font-size: clamp(1.05rem, 1.2vw, 1.35rem);
font-weight: 800;
letter-spacing: .2px;
}
.ib-loyalty-sub {
margin: 4px 0 0 0;
color: var(--text-muted);
font-weight: 500;
font-size: clamp(.9rem, 1vw, 1rem);
}
/* ===== Rewards ===== */
.ib-rewards {
border-radius: var(--radius-lg);
padding: 14px 14px 10px;
background: rgba(255, 255, 255, .04);
border: 1px solid rgba(255, 255, 255, .12);
}
.ib-rewards h6 {
margin: 4px 6px 12px;
font-size: .95rem;
letter-spacing: .25px;
opacity: .95;
}
.ib-rewards-grid {
display: grid;
gap: 10px;
/* auto-fit: se adapta solo */
grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
}
.ib-card {
background: var(--card-bg);
border: 1px solid var(--card-border);
border-radius: var(--radius-md);
padding: 10px 10px 9px;
color: var(--card-title);
box-shadow: 0 8px 18px rgba(0, 0, 0, .10);
}
.ib-card .range {
font-size: .82rem;
opacity: .85;
font-weight: 700;
margin-bottom: 6px;
}
.ib-card .percent {
font-size: 1.35rem;
font-weight: 900;
line-height: 1;
margin: 2px 0 8px;
}
.ib-card .chip {
display: inline-flex;
align-items: center;
justify-content: center;
padding: 5px 10px;
border-radius: 999px;
background: var(--card-chip-bg);
color: var(--card-chip-text);
font-weight: 800;
font-size: .78rem;
letter-spacing: .2px;
}
/* Tarjeta “0%” con borde punteado (si la usas) */
.ib-card.is-empty {
background: rgba(255, 255, 255, .9);
border: 2px dashed rgba(10, 49, 75, .35);
}
/* ===== Ajustes extra para móviles ===== */
@media (max-width: 420px) {
.ib-rewards {
padding: 12px;
}
.ib-card {
padding: 12px;
}
}
/* ===== Libro: reduce presencia o desaparece en móvil ===== */
@media (max-width: 1100px) {
.ib-loyalty-banner .book.small {
opacity: .55;
left: 10px;
bottom: 10px;
width: 120px;
height: 95px;
}
}
@media (max-width: 520px) {
.ib-loyalty-banner .book.small {
display: none;
/* fuera en móviles pequeños */
}
}
/* ===== OPCIONAL: hero más compacto en móvil ===== */
@media (max-width: 520px) {
.ib-loyalty-head {
gap: 10px;
}
.ib-loyalty-logo {
width: 48px;
height: 48px;
}
.ib-loyalty-logo img {
width: 32px;
height: 32px;
}
}

View File

@ -0,0 +1,84 @@
<?xml version="1.0" encoding="iso-8859-1"?>
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
<svg fill="#ffffff" height="800px" width="800px" version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
viewBox="0 0 511 511" xml:space="preserve">
<g>
<path d="M487.5,128.106H479v-24.5c0-2.905-1.678-5.549-4.307-6.786C405.088,64.066,325.408,63.6,255.5,95.371
C185.592,63.6,105.912,64.067,36.307,96.82C33.678,98.057,32,100.701,32,103.606v24.5h-8.5c-12.958,0-23.5,10.542-23.5,23.5v264
c0,12.958,10.542,23.5,23.5,23.5h464c12.958,0,23.5-10.542,23.5-23.5v-264C511,138.648,500.458,128.106,487.5,128.106z
M263,239.583c0-0.009,0-0.019,0-0.028V108.416c64.137-28.707,136.861-28.707,201,0v27.161c0,0.01-0.001,0.02-0.001,0.029
s0.001,0.02,0.001,0.029v244.438c-32.237-13.461-66.371-20.193-100.5-20.193c-34.129,0-68.264,6.732-100.5,20.193V239.583z
M215,96.391c11.187,3.204,22.217,7.198,33,12.025v117.177l-12.34-8.227c-2.52-1.68-5.801-1.68-8.32,0L215,225.593V96.391z
M47,135.626c0-0.007,0.001-0.013,0.001-0.02S47,135.594,47,135.587v-27.171c48.563-21.736,102.046-26.999,153-15.82v32.856
c-26.767-5.505-54.078-6.777-81.328-3.75c-4.117,0.457-7.083,4.165-6.626,8.282c0.458,4.116,4.162,7.085,8.282,6.626
c26.708-2.967,53.479-1.562,79.671,4.165v48.686c-15.912-3.265-32.14-5.067-48.377-5.323c-4.145-0.078-7.552,3.239-7.618,7.38
c-0.065,4.142,3.239,7.552,7.38,7.618c16.331,0.258,32.654,2.164,48.614,5.647v16.66c-43.389-8.909-88.39-6.644-130.748,6.665
c-3.952,1.241-6.148,5.451-4.907,9.403c1.007,3.204,3.964,5.254,7.153,5.254c0.745,0,1.502-0.112,2.25-0.347
c40.908-12.852,84.428-14.773,126.252-5.638v2.825c0,2.766,1.522,5.308,3.961,6.612c2.438,1.306,5.398,1.162,7.699-0.372
l19.84-13.227l16.5,11v136.454c-32.237-13.461-66.371-20.193-100.5-20.193c-34.129,0-68.264,6.732-100.5,20.193V135.626z
M224,424.106H23.5c-4.687,0-8.5-3.813-8.5-8.5v-264c0-4.687,3.813-8.5,8.5-8.5H32v248.5v8c0,4.142,3.358,7.5,7.5,7.5H224V424.106z
M57.29,392.106c58.099-22.934,122.32-22.935,180.42,0H57.29z M272,424.106h-33v-17h33V424.106z M453.71,392.106H273.29
C331.389,369.172,395.61,369.172,453.71,392.106z M496,415.606c0,4.687-3.813,8.5-8.5,8.5H287v-17h184.5c4.142,0,7.5-3.358,7.5-7.5
v-8v-248.5h8.5c4.687,0,8.5,3.813,8.5,8.5V415.606z"/>
<path d="M309.96,317.749c-8.302,1.74-16.615,3.911-24.708,6.454c-3.952,1.242-6.148,5.452-4.907,9.403
c1.007,3.204,3.964,5.254,7.153,5.254c0.745,0,1.502-0.112,2.25-0.347c7.628-2.396,15.464-4.443,23.288-6.083
c4.054-0.85,6.652-4.825,5.802-8.879C317.989,319.497,314.011,316.9,309.96,317.749z"/>
<path d="M439.502,338.859c3.189,0,6.147-2.051,7.153-5.254c1.241-3.952-0.956-8.162-4.907-9.403
c-32.073-10.076-65.329-13.842-98.844-11.188c-4.129,0.326-7.211,3.938-6.885,8.068s3.935,7.213,8.068,6.885
c31.59-2.499,62.935,1.048,93.165,10.546C438,338.748,438.757,338.859,439.502,338.859z"/>
<path d="M287.498,306.767c0.745,0,1.502-0.112,2.25-0.347c48.249-15.159,99.256-15.159,147.504,0
c3.952,1.24,8.162-0.956,9.403-4.907c1.241-3.952-0.956-8.162-4.907-9.403c-51.191-16.083-105.306-16.083-156.496,0
c-3.952,1.241-6.149,5.451-4.907,9.403C281.352,304.716,284.309,306.767,287.498,306.767z"/>
<path d="M287.498,274.859c0.745,0,1.502-0.112,2.25-0.347c27.681-8.697,56.409-12.412,85.399-11.037
c4.147,0.192,7.651-2.999,7.847-7.137c0.196-4.138-2.999-7.65-7.137-7.847c-30.753-1.456-61.236,2.483-90.605,11.71
c-3.952,1.242-6.149,5.452-4.907,9.403C281.352,272.81,284.309,274.859,287.498,274.859z"/>
<path d="M441.748,260.202c-10.76-3.38-21.846-6.086-32.952-8.043c-4.08-0.719-7.968,2.006-8.688,6.085
c-0.719,4.079,2.005,7.969,6.085,8.688c10.467,1.844,20.917,4.395,31.058,7.581c0.749,0.235,1.505,0.347,2.25,0.347
c3.189,0,6.147-2.051,7.153-5.254C447.896,265.653,445.7,261.443,441.748,260.202z"/>
<path d="M287.498,242.767c0.745,0,1.502-0.112,2.25-0.347c48.249-15.159,99.256-15.159,147.504,0
c3.952,1.24,8.162-0.956,9.403-4.907c1.241-3.952-0.956-8.162-4.907-9.403c-51.191-16.083-105.306-16.083-156.496,0
c-3.952,1.241-6.149,5.451-4.907,9.403C281.352,240.716,284.309,242.767,287.498,242.767z"/>
<path d="M334.678,185.702c-16.732,1.858-33.362,5.36-49.426,10.407c-3.952,1.241-6.148,5.451-4.907,9.403
c1.007,3.204,3.964,5.254,7.153,5.254c0.745,0,1.502-0.112,2.25-0.347c15.141-4.757,30.815-8.057,46.585-9.809
c4.117-0.457,7.083-4.165,6.626-8.282S338.79,185.244,334.678,185.702z"/>
<path d="M367.386,199.137c23.725,0.375,47.231,4.17,69.866,11.283c0.748,0.234,1.505,0.347,2.25,0.347
c3.189,0,6.146-2.051,7.153-5.254c1.241-3.952-0.956-8.162-4.907-9.403c-24.015-7.545-48.955-11.572-74.125-11.97
c-4.125-0.078-7.552,3.239-7.618,7.38S363.244,199.072,367.386,199.137z"/>
<path d="M390.671,168.704c4.116,0.46,7.825-2.509,8.282-6.626c0.458-4.117-2.509-7.825-6.626-8.282
c-36.252-4.027-72.278-0.526-107.075,10.406c-3.952,1.242-6.148,5.452-4.907,9.403c1.007,3.204,3.964,5.254,7.153,5.254
c0.745,0,1.502-0.112,2.25-0.347C322.545,168.208,356.5,164.909,390.671,168.704z"/>
<path d="M441.748,164.202c-5.418-1.702-10.96-3.246-16.472-4.588c-4.03-0.98-8.082,1.488-9.062,5.512
c-0.98,4.024,1.488,8.082,5.512,9.062c5.196,1.265,10.419,2.72,15.526,4.324c0.748,0.235,1.505,0.347,2.25,0.347
c3.189,0,6.147-2.051,7.153-5.254C447.896,169.653,445.7,165.443,441.748,164.202z"/>
<path d="M287.498,146.767c0.745,0,1.502-0.112,2.25-0.347c5.103-1.604,10.325-3.058,15.521-4.324
c4.024-0.98,6.492-5.037,5.512-9.062s-5.038-6.492-9.062-5.512c-5.513,1.342-11.053,2.886-16.468,4.587
c-3.951,1.242-6.148,5.452-4.907,9.403C281.352,144.716,284.309,146.767,287.498,146.767z"/>
<path d="M336.329,136.611c34.172-3.796,68.126-0.496,100.923,9.809c0.748,0.234,1.505,0.347,2.25,0.347
c3.189,0,6.146-2.051,7.153-5.254c1.241-3.952-0.956-8.162-4.907-9.403c-34.797-10.933-70.824-14.435-107.076-10.406
c-4.117,0.457-7.083,4.165-6.626,8.282C328.504,134.102,332.21,137.07,336.329,136.611z"/>
<path d="M93.96,317.749c-8.302,1.74-16.615,3.911-24.708,6.454c-3.952,1.242-6.148,5.452-4.907,9.403
c1.007,3.204,3.964,5.254,7.153,5.254c0.745,0,1.502-0.112,2.25-0.347c7.628-2.396,15.464-4.443,23.288-6.083
c4.054-0.85,6.652-4.825,5.802-8.879S98.011,316.9,93.96,317.749z"/>
<path d="M223.502,338.859c3.189,0,6.147-2.051,7.153-5.254c1.241-3.952-0.956-8.162-4.907-9.403
c-32.073-10.076-65.331-13.842-98.844-11.188c-4.129,0.326-7.211,3.938-6.885,8.068s3.934,7.213,8.068,6.885
c31.591-2.499,62.935,1.048,93.165,10.546C222,338.748,222.757,338.859,223.502,338.859z"/>
<path d="M71.498,306.767c0.745,0,1.502-0.112,2.25-0.347c48.249-15.159,99.256-15.159,147.504,0
c3.952,1.24,8.162-0.956,9.403-4.907c1.241-3.952-0.956-8.162-4.907-9.403c-51.191-16.083-105.307-16.083-156.496,0
c-3.952,1.241-6.149,5.451-4.907,9.403C65.352,304.716,68.309,306.767,71.498,306.767z"/>
<path d="M71.498,274.859c0.745,0,1.502-0.112,2.25-0.347c27.681-8.697,56.411-12.412,85.399-11.037
c4.158,0.192,7.65-2.999,7.847-7.137c0.196-4.138-2.999-7.65-7.137-7.847c-30.756-1.456-61.236,2.483-90.605,11.71
c-3.952,1.242-6.149,5.452-4.907,9.403C65.352,272.81,68.309,274.859,71.498,274.859z"/>
<path d="M190.194,266.932c10.467,1.844,20.917,4.395,31.058,7.581c0.749,0.235,1.505,0.347,2.25,0.347
c3.189,0,6.147-2.051,7.153-5.254c1.241-3.952-0.956-8.162-4.907-9.403c-10.76-3.38-21.846-6.086-32.952-8.043
c-4.079-0.719-7.969,2.006-8.688,6.085C183.39,262.323,186.114,266.213,190.194,266.932z"/>
<path d="M118.678,185.702c-16.732,1.858-33.362,5.36-49.426,10.407c-3.952,1.241-6.148,5.451-4.907,9.403
c1.007,3.204,3.964,5.254,7.153,5.254c0.745,0,1.502-0.112,2.25-0.347c15.141-4.757,30.815-8.057,46.585-9.809
c4.117-0.457,7.083-4.165,6.626-8.282C126.503,188.212,122.788,185.244,118.678,185.702z"/>
<path d="M64.345,173.605c1.007,3.204,3.964,5.254,7.153,5.254c0.745,0,1.502-0.112,2.25-0.347
c32.797-10.305,66.752-13.604,100.923-9.809c4.116,0.46,7.825-2.509,8.282-6.626c0.458-4.117-2.509-7.825-6.626-8.282
c-36.253-4.027-72.278-0.526-107.075,10.406C65.3,165.444,63.104,169.654,64.345,173.605z"/>
<path d="M71.498,146.767c0.745,0,1.502-0.112,2.25-0.347c5.103-1.604,10.325-3.058,15.521-4.324
c4.024-0.98,6.492-5.037,5.512-9.062s-5.038-6.492-9.062-5.512c-5.513,1.342-11.053,2.886-16.468,4.587
c-3.951,1.242-6.148,5.452-4.907,9.403C65.352,144.716,68.309,146.767,71.498,146.767z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 8.0 KiB

View File

@ -147,6 +147,55 @@ $(() => {
}); });
}); });
// Botón "Entrar como"
$(document).on('click', '.btn-impersonate-user', function (e) {
e.preventDefault();
const username = $(this).data('username');
const title = window.languageBundle.get(['usuarios.impersonate.title']) || 'Entrar como usuario';
const textTpl = window.languageBundle.get(['usuarios.impersonate.text'])
|| 'Vas a iniciar sesión como <b>{0}</b>.';
const confirmText = window.languageBundle.get(['usuarios.impersonate.button']) || 'Entrar';
Swal.fire({
title,
html: textTpl.replace('{0}', username),
icon: 'warning',
showCancelButton: true,
buttonsStyling: false,
customClass: {
confirmButton: 'btn btn-info w-xs mt-2',
cancelButton: 'btn btn-light w-xs mt-2'
},
confirmButtonText: confirmText,
cancelButtonText: window.languageBundle.get(['app.cancelar']) || 'Cancelar',
}).then((result) => {
if (!result.isConfirmed) return;
$.ajax({
url: '/impersonate',
type: 'POST',
data: { username },
success: function () {
window.location.href = '/';
},
error: function (xhr) {
const msg = (xhr.responseJSON && xhr.responseJSON.message)
|| 'No se pudo iniciar sesión como ese usuario.';
Swal.fire({
icon: 'error',
title: 'No se pudo suplantar',
text: msg,
buttonsStyling: false,
customClass: {
confirmButton: 'btn btn-secondary me-2',
},
});
}
});
});
});
// Submit del form en el modal // Submit del form en el modal
$(document).on('submit', '#userForm', function (e) { $(document).on('submit', '#userForm', function (e) {

View File

@ -0,0 +1,71 @@
<div id="fidelity-banner" th:fragment="home-container-user">
<div class="ib-loyalty-banner">
<!-- Decoraciones -->
<div class="decor"></div>
<div class="book small"></div>
<div class="ib-loyalty-inner">
<!-- Panel principal -->
<div class="ib-loyalty-hero">
<div class="ib-loyalty-head">
<div class="ib-loyalty-logo">
<img src="/assets/images/logo-sm.png" alt="Logo" />
</div>
<div>
<h3 class="ib-loyalty-title">Programa de Fidelidad</h3>
<p class="ib-loyalty-sub">
Aumenta tus compras en los últimos 12 meses y obtén descuentos automáticos.
</p>
</div>
</div>
</div>
<!-- Rewards -->
<div class="ib-rewards">
<h6>Recompensas</h6>
<div class="ib-rewards-grid">
<div class="ib-card ">
<div class="range">Menos de 1.200€</div>
<div class="percent">0%</div>
<span class="chip">Descuento</span>
</div>
<div class="ib-card">
<div class="range">1.200€ 1.999€</div>
<div class="percent">1%</div>
<span class="chip">Descuento</span>
</div>
<div class="ib-card">
<div class="range">2.000€ 2.999€</div>
<div class="percent">2%</div>
<span class="chip">Descuento</span>
</div>
<div class="ib-card">
<div class="range">3.000€ 3.999€</div>
<div class="percent">3%</div>
<span class="chip">Descuento</span>
</div>
<div class="ib-card">
<div class="range">4.000€ 4.999€</div>
<div class="percent">4%</div>
<span class="chip">Descuento</span>
</div>
<div class="ib-card">
<div class="range">más de 5.000€</div>
<div class="percent">5%</div>
<span class="chip">Descuento</span>
</div>
</div>
</div>
</div>
</div>
</div>

View File

@ -8,6 +8,8 @@
<th:block layout:fragment="pagecss"> <th:block layout:fragment="pagecss">
<link th:href="@{/assets/css/presupuestador.css}" rel="stylesheet" <link th:href="@{/assets/css/presupuestador.css}" rel="stylesheet"
th:unless="${#authorization.expression('isAuthenticated()')}" /> th:unless="${#authorization.expression('isAuthenticated()')}" />
<link th:href="@{/assets/css/home.css}" rel="stylesheet"
th:if="${#authorization.expression('isAuthenticated()')}" />
</th:block> </th:block>
</head> </head>
@ -22,7 +24,7 @@
<th:block layout:fragment="content"> <th:block layout:fragment="content">
<div th:if="${#authorization.expression('isAuthenticated()')}"> <div th:if="${#authorization.expression('isAuthenticated()')}">
<div class="container-fluid"> <div class="container-fluid">
<div th:insert="~{imprimelibros/home/home-container-user :: home-container-user}"></div>
</div> </div>
</div> </div>
<div th:unless="${#authorization.expression('isAuthenticated()')}"> <div th:unless="${#authorization.expression('isAuthenticated()')}">

View File

@ -103,6 +103,14 @@
<a class="dropdown-item" href="/apps-chat"><i <a class="dropdown-item" href="/apps-chat"><i
class="mdi mdi-message-text-outline text-muted fs-16 align-middle me-1"></i> class="mdi mdi-message-text-outline text-muted fs-16 align-middle me-1"></i>
<span class="align-middle" th:text="#{app.mensajes}">Mensajes</span></a> <span class="align-middle" th:text="#{app.mensajes}">Mensajes</span></a>
<div sec:authorize="hasRole('PREVIOUS_ADMINISTRATOR')">
<div class="dropdown-divider"></div>
<a class="dropdown-item" href="#"
onclick="document.getElementById('exitImpersonationForm').submit(); return false;">
<i class="mdi mdi-account-switch text-muted fs-16 align-middle me-1"></i>
<span class="align-middle" th:text="#{app.impersonate.exit}">Volver a mi usuario</span>
</a>
</div>
<div class="dropdown-divider"></div> <div class="dropdown-divider"></div>
<a class="dropdown-item" href="#" <a class="dropdown-item" href="#"
onclick="document.getElementById('logoutForm').submit(); return false;"> onclick="document.getElementById('logoutForm').submit(); return false;">
@ -127,7 +135,10 @@
<form id="logoutForm" th:action="@{/logout}" method="post" class="d-none"> <form id="logoutForm" th:action="@{/logout}" method="post" class="d-none">
<input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}"> <input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}">
</form> </form>
<form id="exitImpersonationForm" th:action="@{/impersonate/exit}" method="post" class="d-none">
<input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}">
</form>
</header> </header>
</div> </div>