mirror of
https://git.imnavajas.es/jjimenez/erp-imprimelibros.git
synced 2026-03-01 06:09:13 +00:00
Compare commits
22 Commits
b2026f1cab
...
fix/import
| Author | SHA1 | Date | |
|---|---|---|---|
| 35967b93a0 | |||
| cef0af1bd2 | |||
| 8282c92419 | |||
| 433a055b14 | |||
| fe4d180e2d | |||
| cc2d2ef193 | |||
| 11a5918c37 | |||
| 88769ddaeb | |||
| 9acb105127 | |||
| 6dab15afbc | |||
| a0783c2062 | |||
| d0ccfb5626 | |||
| 2e569a7ffd | |||
| bc8ce4fa81 | |||
| 1bfe0cf3a2 | |||
| 61e55e014f | |||
| 06a3521f6b | |||
| ecf1472f58 | |||
| 48993a34c4 | |||
| a0bf8552f1 | |||
| 562dc2b231 | |||
| 9a49ccf6b8 |
4
.gitignore
vendored
4
.gitignore
vendored
@ -33,4 +33,6 @@ build/
|
|||||||
.vscode/
|
.vscode/
|
||||||
|
|
||||||
### Logs ###
|
### Logs ###
|
||||||
erp-*.log
|
/Logs/
|
||||||
|
erp.log
|
||||||
|
erp*.log
|
||||||
|
|||||||
12303
logs/erp.log
12303
logs/erp.log
File diff suppressed because one or more lines are too long
@ -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())
|
||||||
|
|
||||||
|
|||||||
@ -398,13 +398,12 @@ public class skApiClient {
|
|||||||
public Map<String, Object> checkPedidoEstado(Long presupuestoId, Locale locale) {
|
public Map<String, Object> checkPedidoEstado(Long presupuestoId, Locale locale) {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|
||||||
String jsonResponse = performWithRetry(() -> {
|
String jsonResponse = performWithRetry(() -> {
|
||||||
String url = this.skApiUrl + "api/estado-pedido/" + presupuestoId;
|
String url = this.skApiUrl + "api/estado-pedido/" + presupuestoId;
|
||||||
|
|
||||||
HttpHeaders headers = new HttpHeaders();
|
HttpHeaders headers = new HttpHeaders();
|
||||||
headers.setContentType(MediaType.APPLICATION_JSON);
|
headers.setBearerAuth(authService.getToken());
|
||||||
headers.setBearerAuth(authService.getToken()); // token actualizado
|
headers.setAccept(java.util.List.of(MediaType.APPLICATION_JSON));
|
||||||
|
|
||||||
HttpEntity<Void> entity = new HttpEntity<>(headers);
|
HttpEntity<Void> entity = new HttpEntity<>(headers);
|
||||||
|
|
||||||
@ -420,19 +419,34 @@ public class skApiClient {
|
|||||||
ObjectMapper mapper = new ObjectMapper();
|
ObjectMapper mapper = new ObjectMapper();
|
||||||
JsonNode root = mapper.readTree(jsonResponse);
|
JsonNode root = mapper.readTree(jsonResponse);
|
||||||
|
|
||||||
if (root.get("data") == null) {
|
// ✅ Si falta data, devolvemos mapa sin "estado" (o con estado=null pero con
|
||||||
throw new RuntimeException(
|
// HashMap)
|
||||||
"Sin respuesta desde el servidor del proveedor");
|
if (root == null || root.get("data") == null || root.get("data").isNull()) {
|
||||||
|
Map<String, Object> out = new HashMap<>();
|
||||||
|
out.put("message", "Respuesta sin campo 'data' desde el servidor del proveedor");
|
||||||
|
return out;
|
||||||
}
|
}
|
||||||
|
|
||||||
String estado = root.get("data").asText();
|
String estado = root.get("data").asText();
|
||||||
return Map.of(
|
return Map.of("estado", estado); // aquí NO es null, así que Map.of OK
|
||||||
"estado", estado);
|
|
||||||
|
} catch (HttpClientErrorException ex) {
|
||||||
|
|
||||||
|
if (ex.getStatusCode() == HttpStatus.NOT_FOUND) {
|
||||||
|
// ✅ 404: devolvemos mapa sin "estado" para evitar null en Map.of
|
||||||
|
Map<String, Object> out = new HashMap<>();
|
||||||
|
out.put("notFound", true);
|
||||||
|
out.put("message", "Orden de trabajo no encontrada para presupuestoId=" + presupuestoId);
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw ex;
|
||||||
|
|
||||||
} catch (JsonProcessingException e) {
|
} catch (JsonProcessingException e) {
|
||||||
// Fallback al 80% del ancho
|
// ✅ no parseable (HTML, debugbar, etc.)
|
||||||
return Map.of(
|
Map<String, Object> out = new HashMap<>();
|
||||||
"estado", null);
|
out.put("message", "Respuesta no-JSON o JSON inválido desde el proveedor");
|
||||||
|
return out;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -542,7 +556,7 @@ public class skApiClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public Boolean aceptarFerro(Long presupuestoId, Locale locale) {
|
public Boolean aceptarFerro(Long presupuestoId, Locale locale) {
|
||||||
|
|
||||||
String result = performWithRetry(() -> {
|
String result = performWithRetry(() -> {
|
||||||
String url = this.skApiUrl + "api/aceptar-ferro/" + presupuestoId;
|
String url = this.skApiUrl + "api/aceptar-ferro/" + presupuestoId;
|
||||||
|
|
||||||
@ -576,9 +590,8 @@ public class skApiClient {
|
|||||||
return Boolean.parseBoolean(result);
|
return Boolean.parseBoolean(result);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public Boolean cancelarPedido(Long pedidoId) {
|
public Boolean cancelarPedido(Long pedidoId) {
|
||||||
|
|
||||||
String result = performWithRetry(() -> {
|
String result = performWithRetry(() -> {
|
||||||
String url = this.skApiUrl + "api/cancelar-pedido/" + pedidoId;
|
String url = this.skApiUrl + "api/cancelar-pedido/" + pedidoId;
|
||||||
|
|
||||||
@ -618,12 +631,21 @@ public class skApiClient {
|
|||||||
private String performWithRetry(Supplier<String> request) {
|
private String performWithRetry(Supplier<String> request) {
|
||||||
try {
|
try {
|
||||||
return request.get();
|
return request.get();
|
||||||
|
|
||||||
} catch (HttpClientErrorException.Unauthorized e) {
|
} catch (HttpClientErrorException.Unauthorized e) {
|
||||||
// Token expirado, renovar y reintentar
|
// Token expirado, renovar y reintentar
|
||||||
authService.invalidateToken();
|
authService.invalidateToken();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
return request.get(); // segundo intento
|
return request.get(); // segundo intento
|
||||||
|
|
||||||
} catch (HttpClientErrorException ex) {
|
} catch (HttpClientErrorException ex) {
|
||||||
|
// ✅ IMPORTANTe: si el segundo intento es 404, NO lo envuelvas
|
||||||
|
if (ex.getStatusCode() == HttpStatus.NOT_FOUND) {
|
||||||
|
throw ex;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Si es otro 4xx/5xx, sí lo envolvemos
|
||||||
throw new RuntimeException("La autenticación ha fallado tras renovar el token.", ex);
|
throw new RuntimeException("La autenticación ha fallado tras renovar el token.", ex);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -7,13 +7,18 @@ import org.springframework.web.bind.annotation.GetMapping;
|
|||||||
|
|
||||||
import com.imprimelibros.erp.configurationERP.VariableService;
|
import com.imprimelibros.erp.configurationERP.VariableService;
|
||||||
import com.imprimelibros.erp.i18n.TranslationService;
|
import com.imprimelibros.erp.i18n.TranslationService;
|
||||||
|
import com.imprimelibros.erp.pedidos.Pedido;
|
||||||
|
import com.imprimelibros.erp.pedidos.PedidoRepository;
|
||||||
|
|
||||||
import org.springframework.security.authentication.AnonymousAuthenticationToken;
|
import org.springframework.security.authentication.AnonymousAuthenticationToken;
|
||||||
import org.springframework.security.core.Authentication;
|
import org.springframework.security.core.Authentication;
|
||||||
|
|
||||||
|
import java.security.Principal;
|
||||||
|
import java.time.Instant;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
import com.imprimelibros.erp.common.Utils;
|
||||||
|
|
||||||
@Controller
|
@Controller
|
||||||
public class HomeController {
|
public class HomeController {
|
||||||
@ -22,9 +27,10 @@ public class HomeController {
|
|||||||
private TranslationService translationService;
|
private TranslationService translationService;
|
||||||
@Autowired
|
@Autowired
|
||||||
private VariableService variableService;
|
private VariableService variableService;
|
||||||
|
@Autowired PedidoRepository pedidoRepository;
|
||||||
|
|
||||||
@GetMapping("/")
|
@GetMapping("/")
|
||||||
public String index(Model model, Authentication authentication, Locale locale) {
|
public String index(Model model, Authentication authentication, Principal principal,Locale locale) {
|
||||||
|
|
||||||
boolean isAuthenticated = authentication != null && authentication.isAuthenticated()
|
boolean isAuthenticated = authentication != null && authentication.isAuthenticated()
|
||||||
&& !(authentication instanceof AnonymousAuthenticationToken);
|
&& !(authentication instanceof AnonymousAuthenticationToken);
|
||||||
@ -37,7 +43,8 @@ public class HomeController {
|
|||||||
"presupuesto.impresion-cubierta",
|
"presupuesto.impresion-cubierta",
|
||||||
"presupuesto.impresion-cubierta-help",
|
"presupuesto.impresion-cubierta-help",
|
||||||
"presupuesto.iva-reducido",
|
"presupuesto.iva-reducido",
|
||||||
"presupuesto.iva-reducido-descripcion");
|
"presupuesto.iva-reducido-descripcion",
|
||||||
|
"pedido.gasto-anual");
|
||||||
|
|
||||||
Map<String, String> translations = translationService.getTranslations(locale, keys);
|
Map<String, String> translations = translationService.getTranslations(locale, keys);
|
||||||
model.addAttribute("languageBundle", translations);
|
model.addAttribute("languageBundle", translations);
|
||||||
@ -51,6 +58,11 @@ public class HomeController {
|
|||||||
// empty translations for authenticated users
|
// empty translations for authenticated users
|
||||||
Map<String, String> translations = Map.of();
|
Map<String, String> translations = Map.of();
|
||||||
model.addAttribute("languageBundle", translations);
|
model.addAttribute("languageBundle", translations);
|
||||||
|
|
||||||
|
Instant haceUnAno = Instant.now().minusSeconds(365 * 24 * 60 * 60);
|
||||||
|
Long userId = Utils.currentUserId(principal);
|
||||||
|
double totalGastado = pedidoRepository.sumTotalByCreatedByAndCreatedAtAfter(userId, haceUnAno);
|
||||||
|
model.addAttribute("totalGastado", totalGastado);
|
||||||
}
|
}
|
||||||
return "imprimelibros/home/home";
|
return "imprimelibros/home/home";
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,6 +3,7 @@ package com.imprimelibros.erp.pedidos;
|
|||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
import org.springframework.scheduling.annotation.Scheduled;
|
import org.springframework.scheduling.annotation.Scheduled;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
@ -25,18 +26,34 @@ public class PedidoEstadoService {
|
|||||||
/**
|
/**
|
||||||
* Ejecuta cada noche a las 4:00 AM
|
* Ejecuta cada noche a las 4:00 AM
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
// test @Scheduled(cron = "0 * * * * *")
|
||||||
@Scheduled(cron = "0 0 4 * * *")
|
@Scheduled(cron = "0 0 4 * * *")
|
||||||
public void actualizarEstadosPedidos() {
|
public void actualizarEstadosPedidos() {
|
||||||
|
|
||||||
|
log.info("JOB actualizarEstadosPedidos iniciado");
|
||||||
|
|
||||||
List<PedidoLinea> pedidosLineas = pedidoLineaRepository.findPedidosLineasParaActualizarEstado();
|
List<PedidoLinea> pedidosLineas = pedidoLineaRepository.findPedidosLineasParaActualizarEstado();
|
||||||
|
|
||||||
|
log.info("Pedidos líneas a procesar: {}", pedidosLineas.size());
|
||||||
|
|
||||||
|
|
||||||
for (PedidoLinea linea : pedidosLineas) {
|
for (PedidoLinea linea : pedidosLineas) {
|
||||||
|
|
||||||
|
log.info("Actualizando estado pedidoLineaId={}", linea.getId());
|
||||||
|
|
||||||
try {
|
try {
|
||||||
Map<String, Object> resultado = pedidoService.actualizarEstado(linea.getId(), Locale.getDefault());
|
Map<String, Object> resultado = pedidoService.actualizarEstado(linea.getId(), Locale.getDefault());
|
||||||
|
|
||||||
if (!Boolean.TRUE.equals(resultado.get("success"))) {
|
if (!Boolean.TRUE.equals(resultado.get("success"))) {
|
||||||
log.error("Error al actualizar estado. pedidoLineaId={} message={}",
|
log.error("Error al actualizar estado. pedidoLineaId={} message={}",
|
||||||
linea.getId(), resultado.get("message"));
|
linea.getId(), resultado.get("message"));
|
||||||
|
} else {
|
||||||
|
String msg = String.valueOf(resultado.get("message"));
|
||||||
|
if (msg != null && msg.contains("Orden de trabajo no encontrada")) {
|
||||||
|
log.warn("OT no encontrada. pedidoLineaId={} message={}", linea.getId(), msg);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch (Exception ex) {
|
} catch (Exception ex) {
|
||||||
log.error("Excepción actualizando estado. pedidoLineaId={}", linea.getId(), ex);
|
log.error("Excepción actualizando estado. pedidoLineaId={}", linea.getId(), ex);
|
||||||
|
|||||||
@ -24,10 +24,12 @@ import com.imprimelibros.erp.presupuesto.service.PresupuestoService;
|
|||||||
import com.imprimelibros.erp.users.UserService;
|
import com.imprimelibros.erp.users.UserService;
|
||||||
import com.imprimelibros.erp.direcciones.DireccionService;
|
import com.imprimelibros.erp.direcciones.DireccionService;
|
||||||
import com.imprimelibros.erp.externalApi.skApiClient;
|
import com.imprimelibros.erp.externalApi.skApiClient;
|
||||||
import com.imprimelibros.erp.facturacion.FacturaDireccion;
|
|
||||||
import com.imprimelibros.erp.facturacion.dto.DireccionFacturacionDto;
|
import com.imprimelibros.erp.facturacion.dto.DireccionFacturacionDto;
|
||||||
import com.imprimelibros.erp.pedidos.PedidoLinea.Estado;
|
import com.imprimelibros.erp.pedidos.PedidoLinea.Estado;
|
||||||
|
|
||||||
|
import org.springframework.http.HttpStatus;
|
||||||
|
import org.springframework.web.client.HttpClientErrorException;
|
||||||
|
|
||||||
@Service
|
@Service
|
||||||
public class PedidoService {
|
public class PedidoService {
|
||||||
|
|
||||||
@ -341,14 +343,27 @@ public class PedidoService {
|
|||||||
|
|
||||||
Map<String, Object> result = skApiClient.checkPedidoEstado(refExterna, locale);
|
Map<String, Object> result = skApiClient.checkPedidoEstado(refExterna, locale);
|
||||||
|
|
||||||
if (result == null || result.get("estado") == null) {
|
if (result == null) {
|
||||||
return Map.of(
|
return Map.of(
|
||||||
"success", false,
|
"success", false,
|
||||||
"message", messageSource.getMessage("pedido.errors.update-server-error", null, locale));
|
"message", messageSource.getMessage("pedido.errors.update-server-error", null, locale));
|
||||||
}
|
}
|
||||||
|
|
||||||
String estadoStr = String.valueOf(result.get("estado"));
|
if (Boolean.TRUE.equals(result.get("notFound"))) {
|
||||||
|
return Map.of(
|
||||||
|
"success", true,
|
||||||
|
"message", String.valueOf(result.getOrDefault("message", "OT no encontrada (404). Se omite.")));
|
||||||
|
}
|
||||||
|
|
||||||
|
Object estadoObj = result.get("estado");
|
||||||
|
if (estadoObj == null) {
|
||||||
|
return Map.of(
|
||||||
|
"success", false,
|
||||||
|
"message", messageSource.getMessage("pedido.errors.update-server-error", null, locale));
|
||||||
|
}
|
||||||
|
|
||||||
|
String estadoStr = String.valueOf(estadoObj);
|
||||||
|
|
||||||
PedidoLinea.Estado estadoSk;
|
PedidoLinea.Estado estadoSk;
|
||||||
try {
|
try {
|
||||||
// si la API devuelve minúsculas tipo "produccion", esto funciona
|
// si la API devuelve minúsculas tipo "produccion", esto funciona
|
||||||
|
|||||||
@ -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
|
||||||
@ -509,6 +513,102 @@ public class PresupuestoController {
|
|||||||
return ResponseEntity.ok(resumen);
|
return ResponseEntity.ok(resumen);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@PostMapping("/public/prepare-claim")
|
||||||
|
public ResponseEntity<?> prepareClaim(
|
||||||
|
@RequestBody Map<String, Object> body,
|
||||||
|
HttpServletRequest request) {
|
||||||
|
|
||||||
|
Long presupuestoId = objectMapper.convertValue(body.get("presupuestoId"), Long.class);
|
||||||
|
if (presupuestoId == null) {
|
||||||
|
return ResponseEntity.badRequest().body(Map.of("message", "missing presupuestoId"));
|
||||||
|
}
|
||||||
|
|
||||||
|
Presupuesto p = presupuestoRepository.findById(presupuestoId).orElse(null);
|
||||||
|
if (p == null) {
|
||||||
|
return ResponseEntity.status(HttpStatus.NOT_FOUND)
|
||||||
|
.body(Map.of("message", "presupuesto not found"));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (p.getOrigen() != Presupuesto.Origen.publico) {
|
||||||
|
return ResponseEntity.badRequest().body(Map.of("message", "presupuesto not public"));
|
||||||
|
}
|
||||||
|
|
||||||
|
request.getSession(true).setAttribute("presupuesto_claim_id", presupuestoId);
|
||||||
|
return ResponseEntity.ok(Map.of("success", true));
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/claim")
|
||||||
|
@Transactional
|
||||||
|
public String claimPresupuesto(
|
||||||
|
HttpServletRequest request,
|
||||||
|
Authentication authentication,
|
||||||
|
RedirectAttributes redirectAttributes,
|
||||||
|
Locale locale) {
|
||||||
|
|
||||||
|
Object attr = request.getSession(false) != null
|
||||||
|
? request.getSession(false).getAttribute("presupuesto_claim_id")
|
||||||
|
: null;
|
||||||
|
|
||||||
|
Long presupuestoId = null;
|
||||||
|
if (attr instanceof Long) {
|
||||||
|
presupuestoId = (Long) attr;
|
||||||
|
} else if (attr != null) {
|
||||||
|
try {
|
||||||
|
presupuestoId = Long.valueOf(attr.toString());
|
||||||
|
} catch (NumberFormatException ignore) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (presupuestoId == null) {
|
||||||
|
redirectAttributes.addFlashAttribute("errorMessage",
|
||||||
|
messageSource.getMessage("presupuesto.errores.presupuesto-no-existe", new Object[] { 0 }, locale));
|
||||||
|
return "redirect:/presupuesto";
|
||||||
|
}
|
||||||
|
|
||||||
|
Presupuesto p = presupuestoRepository.findById(presupuestoId).orElse(null);
|
||||||
|
if (p == null) {
|
||||||
|
redirectAttributes.addFlashAttribute("errorMessage",
|
||||||
|
messageSource.getMessage("presupuesto.errores.presupuesto-no-existe",
|
||||||
|
new Object[] { presupuestoId }, locale));
|
||||||
|
return "redirect:/presupuesto";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (p.getUser() != null && authentication != null) {
|
||||||
|
Long currentUserId = null;
|
||||||
|
if (authentication.getPrincipal() instanceof UserDetailsImpl udi) {
|
||||||
|
currentUserId = udi.getId();
|
||||||
|
} else {
|
||||||
|
currentUserId = userRepo.findIdByUserNameIgnoreCase(authentication.getName()).orElse(null);
|
||||||
|
}
|
||||||
|
if (currentUserId != null && p.getUser().getId().equals(currentUserId)) {
|
||||||
|
request.getSession().removeAttribute("presupuesto_claim_id");
|
||||||
|
return "redirect:/presupuesto/edit/" + p.getId();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (p.getOrigen() != Presupuesto.Origen.publico) {
|
||||||
|
redirectAttributes.addFlashAttribute("errorMessage",
|
||||||
|
messageSource.getMessage("presupuesto.errores.presupuesto-no-existe",
|
||||||
|
new Object[] { presupuestoId }, locale));
|
||||||
|
return "redirect:/presupuesto";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (authentication != null) {
|
||||||
|
if (authentication.getPrincipal() instanceof UserDetailsImpl udi) {
|
||||||
|
p.setUser(userRepo.getReferenceById(udi.getId()));
|
||||||
|
} else {
|
||||||
|
userRepo.findByUserNameIgnoreCase(authentication.getName()).ifPresent(p::setUser);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
p.setOrigen(Presupuesto.Origen.privado);
|
||||||
|
p.setEstado(Presupuesto.Estado.borrador);
|
||||||
|
presupuestoRepository.saveAndFlush(p);
|
||||||
|
|
||||||
|
request.getSession().removeAttribute("presupuesto_claim_id");
|
||||||
|
return "redirect:/presupuesto/edit/" + p.getId();
|
||||||
|
}
|
||||||
|
|
||||||
// =============================================
|
// =============================================
|
||||||
// MÉTODOS PARA USUARIOS AUTENTICADOS
|
// MÉTODOS PARA USUARIOS AUTENTICADOS
|
||||||
// =============================================
|
// =============================================
|
||||||
@ -824,6 +924,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),
|
||||||
|
|||||||
@ -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.http.ResponseEntity;
|
||||||
|
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 ResponseEntity<Void> impersonate(
|
||||||
|
@RequestParam("username") String username,
|
||||||
|
Authentication authentication,
|
||||||
|
HttpServletRequest request) {
|
||||||
|
|
||||||
|
if (authentication == null) {
|
||||||
|
return ResponseEntity.status(401).build();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hasRole(authentication, PREVIOUS_ADMIN_ROLE)) {
|
||||||
|
return ResponseEntity.status(409).build();
|
||||||
|
}
|
||||||
|
|
||||||
|
String normalized = sanitizer.plain(username);
|
||||||
|
if (normalized == null || normalized.isBlank()) {
|
||||||
|
return ResponseEntity.badRequest().build();
|
||||||
|
}
|
||||||
|
normalized = normalized.trim().toLowerCase();
|
||||||
|
|
||||||
|
if (authentication.getName() != null
|
||||||
|
&& authentication.getName().equalsIgnoreCase(normalized)) {
|
||||||
|
return ResponseEntity.status(409).build();
|
||||||
|
}
|
||||||
|
|
||||||
|
UserDetails target;
|
||||||
|
try {
|
||||||
|
target = userService.loadUserByUsername(normalized);
|
||||||
|
} catch (UsernameNotFoundException ex) {
|
||||||
|
throw new AccessDeniedException("No autorizado");
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean targetIsSuperAdmin = target.getAuthorities().stream()
|
||||||
|
.anyMatch(a -> "ROLE_SUPERADMIN".equals(a.getAuthority()));
|
||||||
|
if (targetIsSuperAdmin) {
|
||||||
|
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 ResponseEntity.noContent().build();
|
||||||
|
}
|
||||||
|
|
||||||
|
@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()));
|
||||||
|
}
|
||||||
|
}
|
||||||
155
src/main/java/com/imprimelibros/erp/users/ProfileController.java
Normal file
155
src/main/java/com/imprimelibros/erp/users/ProfileController.java
Normal file
@ -0,0 +1,155 @@
|
|||||||
|
package com.imprimelibros.erp.users;
|
||||||
|
|
||||||
|
import java.util.Locale;
|
||||||
|
|
||||||
|
import org.springframework.context.MessageSource;
|
||||||
|
import org.springframework.security.access.prepost.PreAuthorize;
|
||||||
|
import org.springframework.security.core.Authentication;
|
||||||
|
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||||
|
import org.springframework.stereotype.Controller;
|
||||||
|
import org.springframework.ui.Model;
|
||||||
|
import org.springframework.validation.BindingResult;
|
||||||
|
import org.springframework.validation.annotation.Validated;
|
||||||
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
|
import org.springframework.web.bind.annotation.ModelAttribute;
|
||||||
|
import org.springframework.web.bind.annotation.PostMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestParam;
|
||||||
|
import org.springframework.web.servlet.mvc.support.RedirectAttributes;
|
||||||
|
|
||||||
|
import com.imprimelibros.erp.config.Sanitizer;
|
||||||
|
import com.imprimelibros.erp.users.validation.ProfileForm;
|
||||||
|
|
||||||
|
@Controller
|
||||||
|
@RequestMapping("/pages-profile")
|
||||||
|
@PreAuthorize("isAuthenticated()")
|
||||||
|
public class ProfileController {
|
||||||
|
|
||||||
|
private final UserDao userDao;
|
||||||
|
private final PasswordEncoder passwordEncoder;
|
||||||
|
private final MessageSource messageSource;
|
||||||
|
private final Sanitizer sanitizer;
|
||||||
|
|
||||||
|
public ProfileController(UserDao userDao, PasswordEncoder passwordEncoder,
|
||||||
|
MessageSource messageSource, Sanitizer sanitizer) {
|
||||||
|
this.userDao = userDao;
|
||||||
|
this.passwordEncoder = passwordEncoder;
|
||||||
|
this.messageSource = messageSource;
|
||||||
|
this.sanitizer = sanitizer;
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping
|
||||||
|
public String view(
|
||||||
|
Authentication authentication,
|
||||||
|
@RequestParam(name = "success", required = false) String success,
|
||||||
|
Model model,
|
||||||
|
Locale locale) {
|
||||||
|
|
||||||
|
if (authentication == null) {
|
||||||
|
return "redirect:/login";
|
||||||
|
}
|
||||||
|
|
||||||
|
User user = userDao.findByUserNameIgnoreCase(authentication.getName()).orElse(null);
|
||||||
|
if (user == null) {
|
||||||
|
return "redirect:/login";
|
||||||
|
}
|
||||||
|
|
||||||
|
ProfileForm form = new ProfileForm();
|
||||||
|
form.setId(user.getId());
|
||||||
|
form.setFullName(user.getFullName());
|
||||||
|
form.setUserName(user.getUserName());
|
||||||
|
|
||||||
|
model.addAttribute("user", form);
|
||||||
|
model.addAttribute("success", success != null);
|
||||||
|
return "imprimelibros/users/profile";
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping
|
||||||
|
public String update(
|
||||||
|
Authentication authentication,
|
||||||
|
@Validated @ModelAttribute("user") ProfileForm form,
|
||||||
|
BindingResult binding,
|
||||||
|
Model model,
|
||||||
|
RedirectAttributes redirectAttributes,
|
||||||
|
Locale locale) {
|
||||||
|
|
||||||
|
if (authentication == null) {
|
||||||
|
return "redirect:/login";
|
||||||
|
}
|
||||||
|
|
||||||
|
User user = userDao.findByUserNameIgnoreCase(authentication.getName()).orElse(null);
|
||||||
|
if (user == null) {
|
||||||
|
return "redirect:/login";
|
||||||
|
}
|
||||||
|
|
||||||
|
String normalized = sanitizer.plain(form.getUserName());
|
||||||
|
if (normalized != null) {
|
||||||
|
normalized = normalized.trim().toLowerCase();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (normalized == null || normalized.isBlank()) {
|
||||||
|
binding.rejectValue("userName", "usuarios.error.email",
|
||||||
|
messageSource.getMessage("usuarios.error.email", null, locale));
|
||||||
|
} else if (userDao.existsByUserNameIgnoreCaseAndIdNot(normalized, user.getId())) {
|
||||||
|
binding.rejectValue("userName", "usuarios.error.duplicado",
|
||||||
|
messageSource.getMessage("usuarios.error.duplicado", null, locale));
|
||||||
|
}
|
||||||
|
|
||||||
|
String cleanName = sanitizer.plain(form.getFullName());
|
||||||
|
if (cleanName == null || cleanName.isBlank()) {
|
||||||
|
binding.rejectValue("fullName", "usuarios.error.nombre",
|
||||||
|
messageSource.getMessage("usuarios.error.nombre", null, locale));
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean wantsPasswordChange = hasText(form.getCurrentPassword())
|
||||||
|
|| hasText(form.getNewPassword())
|
||||||
|
|| hasText(form.getConfirmPassword());
|
||||||
|
|
||||||
|
if (wantsPasswordChange) {
|
||||||
|
if (!hasText(form.getCurrentPassword())) {
|
||||||
|
binding.rejectValue("currentPassword", "usuarios.error.password.actual",
|
||||||
|
messageSource.getMessage("usuarios.error.password.actual", null, locale));
|
||||||
|
} else if (!passwordEncoder.matches(form.getCurrentPassword(), user.getPassword())) {
|
||||||
|
binding.rejectValue("currentPassword", "usuarios.error.password.actual.incorrecta",
|
||||||
|
messageSource.getMessage("usuarios.error.password.actual.incorrecta", null, locale));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!hasText(form.getNewPassword())) {
|
||||||
|
binding.rejectValue("newPassword", "usuarios.error.password.nueva.requerida",
|
||||||
|
messageSource.getMessage("usuarios.error.password.nueva.requerida", null, locale));
|
||||||
|
} else if (form.getNewPassword().length() < 6) {
|
||||||
|
binding.rejectValue("newPassword", "usuarios.error.password.min",
|
||||||
|
messageSource.getMessage("usuarios.error.password.min", null, locale));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!hasText(form.getConfirmPassword())) {
|
||||||
|
binding.rejectValue("confirmPassword", "usuarios.error.confirmPassword.requerida",
|
||||||
|
messageSource.getMessage("usuarios.error.confirmPassword.requerida", null, locale));
|
||||||
|
} else if (hasText(form.getNewPassword()) && !form.getNewPassword().equals(form.getConfirmPassword())) {
|
||||||
|
binding.rejectValue("confirmPassword", "usuarios.error.password-coinciden",
|
||||||
|
messageSource.getMessage("usuarios.error.password-coinciden", null, locale));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (binding.hasErrors()) {
|
||||||
|
model.addAttribute("success", false);
|
||||||
|
return "imprimelibros/users/profile";
|
||||||
|
}
|
||||||
|
|
||||||
|
user.setFullName(cleanName.trim());
|
||||||
|
user.setUserName(normalized);
|
||||||
|
|
||||||
|
if (wantsPasswordChange) {
|
||||||
|
user.setPassword(passwordEncoder.encode(form.getNewPassword()));
|
||||||
|
}
|
||||||
|
|
||||||
|
userDao.save(user);
|
||||||
|
|
||||||
|
redirectAttributes.addAttribute("success", "1");
|
||||||
|
return "redirect:/pages-profile";
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean hasText(String value) {
|
||||||
|
return value != null && !value.isBlank();
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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 && !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:
|
||||||
|
|||||||
@ -0,0 +1,68 @@
|
|||||||
|
package com.imprimelibros.erp.users.validation;
|
||||||
|
|
||||||
|
import jakarta.validation.constraints.Email;
|
||||||
|
import jakarta.validation.constraints.NotBlank;
|
||||||
|
|
||||||
|
public class ProfileForm {
|
||||||
|
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
@NotBlank(message = "{usuarios.error.nombre}")
|
||||||
|
private String fullName;
|
||||||
|
|
||||||
|
@NotBlank(message = "{usuarios.error.email}")
|
||||||
|
@Email(message = "{usuarios.error.email.formato}")
|
||||||
|
private String userName;
|
||||||
|
|
||||||
|
private String currentPassword;
|
||||||
|
private String newPassword;
|
||||||
|
private String confirmPassword;
|
||||||
|
|
||||||
|
public Long getId() {
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setId(Long id) {
|
||||||
|
this.id = id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getFullName() {
|
||||||
|
return fullName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setFullName(String fullName) {
|
||||||
|
this.fullName = fullName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getUserName() {
|
||||||
|
return userName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setUserName(String userName) {
|
||||||
|
this.userName = userName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getCurrentPassword() {
|
||||||
|
return currentPassword;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setCurrentPassword(String currentPassword) {
|
||||||
|
this.currentPassword = currentPassword;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getNewPassword() {
|
||||||
|
return newPassword;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setNewPassword(String newPassword) {
|
||||||
|
this.newPassword = newPassword;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getConfirmPassword() {
|
||||||
|
return confirmPassword;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setConfirmPassword(String confirmPassword) {
|
||||||
|
this.confirmPassword = confirmPassword;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -33,7 +33,7 @@ logging.pattern.console=%d{yyyy-MM-dd HH:mm:ss} %-5level [%thread] %logger{36} -
|
|||||||
logging.pattern.file=%d{yyyy-MM-dd HH:mm:ss} %-5level [%thread] %logger{36} - %msg%n
|
logging.pattern.file=%d{yyyy-MM-dd HH:mm:ss} %-5level [%thread] %logger{36} - %msg%n
|
||||||
|
|
||||||
# Datos de la API de Safekat
|
# Datos de la API de Safekat
|
||||||
safekat.api.url=http://localhost:8000/
|
safekat.api.url=https://erp-dev.safekat.es/
|
||||||
safekat.api.email=imnavajas@coit.es
|
safekat.api.email=imnavajas@coit.es
|
||||||
safekat.api.password=Safekat2024
|
safekat.api.password=Safekat2024
|
||||||
|
|
||||||
@ -41,6 +41,6 @@ safekat.api.password=Safekat2024
|
|||||||
redsys.environment=test
|
redsys.environment=test
|
||||||
redsys.url=https://sis-t.redsys.es:25443/sis/realizarPago
|
redsys.url=https://sis-t.redsys.es:25443/sis/realizarPago
|
||||||
redsys.refund.url=https://sis-t.redsys.es:25443/sis/rest/trataPeticionREST
|
redsys.refund.url=https://sis-t.redsys.es:25443/sis/rest/trataPeticionREST
|
||||||
redsys.urls.ok=http://localhost:8080/pagos/redsys/ok
|
redsys.urls.ok=https://imprimelibros.jjimenez.eu/pagos/redsys/ok
|
||||||
redsys.urls.ko=http://localhost:8080/pagos/redsys/ko
|
redsys.urls.ko=https://imprimelibros.jjimenez.eu/pagos/redsys/ko
|
||||||
redsys.urls.notify=https://orological-sacrilegiously-lucille.ngrok-free.dev/pagos/redsys/notify
|
redsys.urls.notify=https://imprimelibros.jjimenez.eu/pagos/redsys/notify
|
||||||
46
src/main/resources/application-local.properties
Normal file
46
src/main/resources/application-local.properties
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
# Profile desarrollo
|
||||||
|
|
||||||
|
#
|
||||||
|
# Logging
|
||||||
|
#
|
||||||
|
logging.level.root=INFO
|
||||||
|
logging.level.org.springframework.security=ERROR
|
||||||
|
logging.level.org.springframework=ERROR
|
||||||
|
logging.level.org.springframework.web=ERROR
|
||||||
|
logging.level.org.thymeleaf=ERROR
|
||||||
|
logging.level.org.apache.catalina.core=ERROR
|
||||||
|
# Debug JPA / Hibernate
|
||||||
|
#logging.level.org.hibernate.SQL=DEBUG
|
||||||
|
#logging.level.org.hibernate.orm.jdbc.bind=TRACE
|
||||||
|
#spring.jpa.properties.hibernate.format_sql=true
|
||||||
|
|
||||||
|
server.error.include-message=always
|
||||||
|
server.error.include-stacktrace=on_param
|
||||||
|
server.error.include-binding-errors=on_param
|
||||||
|
|
||||||
|
|
||||||
|
# Archivo relativo a tu proyecto (asegúrate de que exista el directorio ./logs)
|
||||||
|
logging.file.name=logs/erp.log
|
||||||
|
|
||||||
|
# Rotación tiempo+tamaño (mismo patrón, pero en ./logs)
|
||||||
|
logging.logback.rollingpolicy.file-name-pattern=logs/erp-%d{yyyy-MM-dd}.%i.log
|
||||||
|
logging.logback.rollingpolicy.max-file-size=10MB
|
||||||
|
logging.logback.rollingpolicy.max-history=10
|
||||||
|
logging.logback.rollingpolicy.total-size-cap=1GB
|
||||||
|
|
||||||
|
# Formatos con timestamp
|
||||||
|
logging.pattern.console=%d{yyyy-MM-dd HH:mm:ss} %-5level [%thread] %logger{36} - %msg%n
|
||||||
|
logging.pattern.file=%d{yyyy-MM-dd HH:mm:ss} %-5level [%thread] %logger{36} - %msg%n
|
||||||
|
|
||||||
|
# Datos de la API de Safekat
|
||||||
|
safekat.api.url=http://localhost:8000/
|
||||||
|
safekat.api.email=imnavajas@coit.es
|
||||||
|
safekat.api.password=Safekat2024
|
||||||
|
|
||||||
|
# Configuración Redsys
|
||||||
|
redsys.environment=test
|
||||||
|
redsys.url=https://sis-t.redsys.es:25443/sis/realizarPago
|
||||||
|
redsys.refund.url=https://sis-t.redsys.es:25443/sis/rest/trataPeticionREST
|
||||||
|
redsys.urls.ok=http://localhost:8080/pagos/redsys/ok
|
||||||
|
redsys.urls.ko=http://localhost:8080/pagos/redsys/ko
|
||||||
|
redsys.urls.notify=https://orological-sacrilegiously-lucille.ngrok-free.dev/pagos/redsys/notify
|
||||||
@ -1,7 +1,8 @@
|
|||||||
spring.application.name=erp
|
spring.application.name=erp
|
||||||
# Active profile
|
# Active profile
|
||||||
|
spring.profiles.active=local
|
||||||
#spring.profiles.active=dev
|
#spring.profiles.active=dev
|
||||||
spring.profiles.active=test
|
#spring.profiles.active=test
|
||||||
#spring.profiles.active=prod
|
#spring.profiles.active=prod
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -51,6 +51,8 @@ pedido.table.importe=Importe
|
|||||||
pedido.table.estado=Estado
|
pedido.table.estado=Estado
|
||||||
pedido.table.acciones=Acciones
|
pedido.table.acciones=Acciones
|
||||||
|
|
||||||
|
pedido.gasto-anual=Gasto últimos 12 meses
|
||||||
|
|
||||||
pedido.view.tirada=Tirada
|
pedido.view.tirada=Tirada
|
||||||
pedido.view.view-presupuesto=Ver presupuesto
|
pedido.view.view-presupuesto=Ver presupuesto
|
||||||
pedido.view.aceptar-ferro=Aceptar ferro
|
pedido.view.aceptar-ferro=Aceptar ferro
|
||||||
|
|||||||
@ -1 +1,23 @@
|
|||||||
|
usuarios.form.nombre=Full name
|
||||||
|
usuarios.form.email=Email
|
||||||
|
usuarios.form.confirmarPassword=Confirm password
|
||||||
|
usuarios.form.password.actual=Current password
|
||||||
|
usuarios.form.password.nueva=New password
|
||||||
|
usuarios.form.password.nota=You can only change the password if you provide the current one.
|
||||||
|
|
||||||
|
usuarios.error.nombre=Name is required.
|
||||||
|
usuarios.error.email=Email is required.
|
||||||
|
usuarios.error.email.formato=Email is not valid.
|
||||||
|
usuarios.error.password.min=Password must be at least 6 characters.
|
||||||
|
usuarios.error.password.actual=Current password is required.
|
||||||
|
usuarios.error.password.actual.incorrecta=Current password is not correct.
|
||||||
|
usuarios.error.password.nueva.requerida=New password is required.
|
||||||
|
usuarios.error.confirmPassword.requerida=Password confirmation is required.
|
||||||
|
usuarios.error.password-coinciden=Passwords do not match.
|
||||||
|
usuarios.error.duplicado=There is already a user with that email.
|
||||||
|
|
||||||
|
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
|
||||||
|
usuarios.profile.title=Edit profile
|
||||||
|
usuarios.profile.success=Profile updated successfully.
|
||||||
|
|||||||
@ -20,6 +20,9 @@ usuarios.form.nombre=Nombre completo
|
|||||||
usuarios.form.email=Correo electrónico
|
usuarios.form.email=Correo electrónico
|
||||||
usuarios.form.password=Contraseña
|
usuarios.form.password=Contraseña
|
||||||
usuarios.form.confirmarPassword=Confirmar contraseña
|
usuarios.form.confirmarPassword=Confirmar contraseña
|
||||||
|
usuarios.form.password.actual=Contraseña actual
|
||||||
|
usuarios.form.password.nueva=Nueva contraseña
|
||||||
|
usuarios.form.password.nota=Solo podrás cambiar la contraseña si indicas la actual.
|
||||||
usuarios.form.rol=Rol
|
usuarios.form.rol=Rol
|
||||||
usuarios.form.estado=Estado
|
usuarios.form.estado=Estado
|
||||||
|
|
||||||
@ -37,6 +40,9 @@ usuarios.error.email.formato=El correo electrónico no es válido.
|
|||||||
usuarios.error.rol=El rol seleccionado no es válido.
|
usuarios.error.rol=El rol seleccionado no es válido.
|
||||||
usuarios.error.password.requerida=La contraseña es obligatoria.
|
usuarios.error.password.requerida=La contraseña es obligatoria.
|
||||||
usuarios.error.password.min=La contraseña debe tener al menos 6 caracteres.
|
usuarios.error.password.min=La contraseña debe tener al menos 6 caracteres.
|
||||||
|
usuarios.error.password.actual=La contraseña actual es obligatoria.
|
||||||
|
usuarios.error.password.actual.incorrecta=La contraseña actual no es correcta.
|
||||||
|
usuarios.error.password.nueva.requerida=La nueva contraseña es obligatoria.
|
||||||
usuarios.error.confirmPassword.requerida=La confirmación de la contraseña es obligatoria.
|
usuarios.error.confirmPassword.requerida=La confirmación de la contraseña es obligatoria.
|
||||||
usuarios.error.password-coinciden=Las contraseñas no coinciden.
|
usuarios.error.password-coinciden=Las contraseñas no coinciden.
|
||||||
usuarios.error.delete-relational-data=No se puede eliminar el usuario porque tiene datos relacionados.
|
usuarios.error.delete-relational-data=No se puede eliminar el usuario porque tiene datos relacionados.
|
||||||
@ -53,4 +59,9 @@ 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
|
||||||
|
usuarios.profile.title=Editar perfil
|
||||||
|
usuarios.profile.success=Perfil actualizado correctamente.
|
||||||
|
|||||||
372
src/main/resources/static/assets/css/home.css
Normal file
372
src/main/resources/static/assets/css/home.css
Normal file
@ -0,0 +1,372 @@
|
|||||||
|
: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;
|
||||||
|
|
||||||
|
/* Nunca más de 3 columnas, pero baja si no hay sitio */
|
||||||
|
grid-template-columns: repeat(3, minmax(160px, 1fr));
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 992px){
|
||||||
|
.ib-rewards-grid{
|
||||||
|
grid-template-columns: repeat(2, minmax(160px, 1fr));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 520px){
|
||||||
|
.ib-rewards-grid{
|
||||||
|
grid-template-columns: 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.ib-loyalty-left{
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 18px;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ib-loyalty-stat-card{
|
||||||
|
|
||||||
|
/* centra en el espacio libre */
|
||||||
|
margin-top: auto;
|
||||||
|
margin-bottom: auto;
|
||||||
|
|
||||||
|
margin-left: auto;
|
||||||
|
margin-right: auto;
|
||||||
|
|
||||||
|
|
||||||
|
/* mismo look que el hero */
|
||||||
|
background: var(--banner-panel-bg);
|
||||||
|
border: 2px solid var(--banner-panel-border);
|
||||||
|
border-radius: var(--radius-lg);
|
||||||
|
|
||||||
|
padding: 22px;
|
||||||
|
max-width: 360px; /* evita que se haga enorme */
|
||||||
|
}
|
||||||
|
|
||||||
|
.ib-loyalty-stat-card h6{
|
||||||
|
letter-spacing: .4px;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ib-loyalty-stat-card h2{
|
||||||
|
font-weight: 800;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ib-loyalty-stat-card i{
|
||||||
|
opacity: .85;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 1100px){
|
||||||
|
.ib-loyalty-stat-card{
|
||||||
|
margin-top: 12px;
|
||||||
|
margin-bottom: 0;
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
84
src/main/resources/static/assets/images/open-book.svg
Normal file
84
src/main/resources/static/assets/images/open-book.svg
Normal 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 |
@ -0,0 +1,34 @@
|
|||||||
|
import {formateaMoneda} from "./utils.js";
|
||||||
|
|
||||||
|
$(() => {
|
||||||
|
// Contador animado
|
||||||
|
function counter() {
|
||||||
|
var counter = document.querySelectorAll(".counter-value");
|
||||||
|
var speed = 250; // The lower the slower
|
||||||
|
counter &&
|
||||||
|
Array.from(counter).forEach(function (counter_value) {
|
||||||
|
function updateCount() {
|
||||||
|
var target = +counter_value.getAttribute("data-target");
|
||||||
|
var count = +counter_value.innerText;
|
||||||
|
var inc = target / speed;
|
||||||
|
if (inc < 1) {
|
||||||
|
inc = 1;
|
||||||
|
}
|
||||||
|
// Check if target is reached
|
||||||
|
if (count < target) {
|
||||||
|
// Add inc to count and output in counter_value
|
||||||
|
counter_value.innerText = (count + inc).toFixed(0);
|
||||||
|
// Call function every ms
|
||||||
|
setTimeout(updateCount, 1);
|
||||||
|
} else {
|
||||||
|
counter_value.innerText = formateaMoneda(target);
|
||||||
|
}
|
||||||
|
formateaMoneda(counter_value.innerText);
|
||||||
|
}
|
||||||
|
updateCount();
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
||||||
|
counter();
|
||||||
|
|
||||||
|
})
|
||||||
@ -318,6 +318,50 @@ export default class PresupuestoWizard {
|
|||||||
document.body.removeChild(a);
|
document.body.removeChild(a);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
$(document)
|
||||||
|
.off('click.login-required', '.btn-login-required')
|
||||||
|
.on('click.login-required', '.btn-login-required', async (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
const rawId = this.opts.presupuestoId || window.PRESUPUESTO_ID || $('#presupuesto_id').val();
|
||||||
|
const presupuestoId = rawId ? parseInt(rawId, 10) : null;
|
||||||
|
|
||||||
|
if (!presupuestoId || Number.isNaN(presupuestoId)) {
|
||||||
|
Swal.fire({
|
||||||
|
icon: 'error',
|
||||||
|
title: 'No se encontró el presupuesto',
|
||||||
|
text: 'Vuelve a generar el resumen e inténtalo de nuevo.',
|
||||||
|
buttonsStyling: false,
|
||||||
|
customClass: {
|
||||||
|
confirmButton: 'btn btn-secondary me-2',
|
||||||
|
cancelButton: 'btn btn-light'
|
||||||
|
},
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await $.ajax({
|
||||||
|
url: '/presupuesto/public/prepare-claim',
|
||||||
|
type: 'POST',
|
||||||
|
contentType: 'application/json',
|
||||||
|
data: JSON.stringify({ presupuestoId })
|
||||||
|
});
|
||||||
|
window.location.assign('/presupuesto/claim');
|
||||||
|
} catch (err) {
|
||||||
|
Swal.fire({
|
||||||
|
icon: 'error',
|
||||||
|
title: 'No se pudo continuar',
|
||||||
|
text: 'Inténtalo de nuevo en unos segundos.',
|
||||||
|
buttonsStyling: false,
|
||||||
|
customClass: {
|
||||||
|
confirmButton: 'btn btn-secondary me-2',
|
||||||
|
cancelButton: 'btn btn-light'
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1853,6 +1897,15 @@ export default class PresupuestoWizard {
|
|||||||
...result,
|
...result,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (!this.formData.servicios.servicios.some(s => s.id === "marcapaginas") && result.precio > 0) {
|
||||||
|
this.formData.servicios.servicios.push({
|
||||||
|
id: "marcapaginas",
|
||||||
|
label: $(`label[for="marcapaginas"] .service-title`).text().trim(),
|
||||||
|
units: 1,
|
||||||
|
price: result.precio,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
this.#cacheFormData();
|
this.#cacheFormData();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -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) {
|
||||||
|
|||||||
@ -0,0 +1,105 @@
|
|||||||
|
<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">
|
||||||
|
|
||||||
|
<!-- ===================== -->
|
||||||
|
<!-- COLUMNA IZQUIERDA -->
|
||||||
|
<!-- ===================== -->
|
||||||
|
<div class="ib-loyalty-left">
|
||||||
|
|
||||||
|
<!-- PANEL SUPERIOR: TÍTULO -->
|
||||||
|
<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>
|
||||||
|
|
||||||
|
<!-- PANEL INFERIOR: ESTADÍSTICA (RECUADRO INDEPENDIENTE) -->
|
||||||
|
<div class="ib-loyalty-stat-card">
|
||||||
|
|
||||||
|
<h6 class="text-uppercase fs-13 mb-3">
|
||||||
|
<span th:text="#{pedido.gasto-anual}">Gasto últimos 12 meses</span>
|
||||||
|
<i class="ri-arrow-up-circle-line text-success fs-18 float-end align-middle"></i>
|
||||||
|
</h6>
|
||||||
|
|
||||||
|
<div class="d-flex align-items-center">
|
||||||
|
<div class="flex-shrink-0">
|
||||||
|
<i class="ri-money-euro-circle-line display-6 "></i>
|
||||||
|
</div>
|
||||||
|
<div class="flex-grow-1 ms-3">
|
||||||
|
<h2 class="mb-0">
|
||||||
|
<h2 class="mb-0"><span class="counter-value" th:attr="data-target=${totalGastado}">0</span></h2>
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ===================== -->
|
||||||
|
<!-- COLUMNA DERECHA -->
|
||||||
|
<!-- ===================== -->
|
||||||
|
<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>
|
||||||
@ -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()')}">
|
||||||
@ -41,6 +43,9 @@
|
|||||||
<script type="module"
|
<script type="module"
|
||||||
th:src="@{/assets/js/pages/imprimelibros/presupuestador/presupuesto-marcapaginas.js}"></script>
|
th:src="@{/assets/js/pages/imprimelibros/presupuestador/presupuesto-marcapaginas.js}"></script>
|
||||||
</div>
|
</div>
|
||||||
|
<div th:if="${#authorization.expression('isAuthenticated()')}">
|
||||||
|
<script type="module" th:src="@{/assets/js/pages/imprimelibros/counter-widget.js}"></script>
|
||||||
|
</div>
|
||||||
<script th:inline="javascript">
|
<script th:inline="javascript">
|
||||||
window.languageBundle = /*[[${languageBundle}]]*/ {};
|
window.languageBundle = /*[[${languageBundle}]]*/ {};
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@ -100,9 +100,14 @@
|
|||||||
<a class="dropdown-item" href="/pages-profile"><i
|
<a class="dropdown-item" href="/pages-profile"><i
|
||||||
class="mdi mdi-account-circle text-muted fs-16 align-middle me-1"></i> <span
|
class="mdi mdi-account-circle text-muted fs-16 align-middle me-1"></i> <span
|
||||||
class="align-middle" th:text="#{app.perfil}">Perfil</span></a>
|
class="align-middle" th:text="#{app.perfil}">Perfil</span></a>
|
||||||
<a class="dropdown-item" href="/apps-chat"><i
|
<div sec:authorize="hasRole('PREVIOUS_ADMINISTRATOR')">
|
||||||
class="mdi mdi-message-text-outline text-muted fs-16 align-middle me-1"></i>
|
<div class="dropdown-divider"></div>
|
||||||
<span class="align-middle" th:text="#{app.mensajes}">Mensajes</span></a>
|
<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 +132,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>
|
||||||
|
|||||||
107
src/main/resources/templates/imprimelibros/users/profile.html
Normal file
107
src/main/resources/templates/imprimelibros/users/profile.html
Normal file
@ -0,0 +1,107 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html xmlns:th="http://www.thymeleaf.org" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
|
||||||
|
layout:decorate="~{imprimelibros/layout}">
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<th:block layout:fragment="pagetitle" />
|
||||||
|
<th:block th:replace="~{imprimelibros/partials/head-css :: head-css}" />
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<div th:replace="~{imprimelibros/partials/topbar :: topbar}" />
|
||||||
|
<div th:replace="~{imprimelibros/partials/sidebar :: sidebar}" />
|
||||||
|
|
||||||
|
<th:block layout:fragment="content">
|
||||||
|
<div th:if="${#authorization.expression('isAuthenticated()')}">
|
||||||
|
|
||||||
|
<nav aria-label="breadcrumb">
|
||||||
|
<ol class="breadcrumb">
|
||||||
|
<li class="breadcrumb-item"><a href="/"><i class="ri-home-5-fill"></i></a></li>
|
||||||
|
<li class="breadcrumb-item active" aria-current="page" th:text="#{app.perfil}">Perfil</li>
|
||||||
|
</ol>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<div class="container-fluid">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-12 col-lg-6">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h5 class="card-title mb-0" th:text="#{usuarios.profile.title}">Editar perfil</h5>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
|
||||||
|
<div th:if="${success}" class="alert alert-success"
|
||||||
|
th:text="#{usuarios.profile.success}">Perfil actualizado.</div>
|
||||||
|
|
||||||
|
<form id="profileForm" novalidate th:action="@{/pages-profile}" th:object="${user}"
|
||||||
|
method="post">
|
||||||
|
|
||||||
|
<div th:if="${#fields.hasGlobalErrors()}" class="alert alert-danger">
|
||||||
|
<div th:each="e : ${#fields.globalErrors()}" th:text="${e}"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label th:text="#{usuarios.form.nombre}" for="fullName">Nombre</label>
|
||||||
|
<input type="text" class="form-control" id="fullName" th:field="*{fullName}"
|
||||||
|
th:classappend="${#fields.hasErrors('fullName')} ? ' is-invalid'" required>
|
||||||
|
<div class="invalid-feedback" th:if="${#fields.hasErrors('fullName')}"
|
||||||
|
th:errors="*{fullName}">Error</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label th:text="#{usuarios.form.email}" for="userName">Correo electrónico</label>
|
||||||
|
<input type="email" class="form-control" id="userName" th:field="*{userName}"
|
||||||
|
th:classappend="${#fields.hasErrors('userName')} ? ' is-invalid'" required>
|
||||||
|
<div class="invalid-feedback" th:if="${#fields.hasErrors('userName')}"
|
||||||
|
th:errors="*{userName}">Error</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<hr class="my-4">
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label th:text="#{usuarios.form.password.actual}" for="currentPassword">Contraseña actual</label>
|
||||||
|
<input type="password" class="form-control" id="currentPassword"
|
||||||
|
th:field="*{currentPassword}"
|
||||||
|
th:classappend="${#fields.hasErrors('currentPassword')} ? ' is-invalid'">
|
||||||
|
<div class="invalid-feedback" th:if="${#fields.hasErrors('currentPassword')}"
|
||||||
|
th:errors="*{currentPassword}">Error</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label th:text="#{usuarios.form.password.nueva}" for="newPassword">Nueva contraseña</label>
|
||||||
|
<input type="password" class="form-control" id="newPassword"
|
||||||
|
th:field="*{newPassword}"
|
||||||
|
th:classappend="${#fields.hasErrors('newPassword')} ? ' is-invalid'">
|
||||||
|
<div class="text-muted" th:text="#{usuarios.form.password.nota}">
|
||||||
|
Solo podrás cambiar la contraseña si indicas la actual.
|
||||||
|
</div>
|
||||||
|
<div class="invalid-feedback" th:if="${#fields.hasErrors('newPassword')}"
|
||||||
|
th:errors="*{newPassword}">Error</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label th:text="#{usuarios.form.confirmarPassword}" for="confirmPassword">Confirmar contraseña</label>
|
||||||
|
<input type="password" class="form-control" id="confirmPassword"
|
||||||
|
th:field="*{confirmPassword}"
|
||||||
|
th:classappend="${#fields.hasErrors('confirmPassword')} ? ' is-invalid'">
|
||||||
|
<div class="invalid-feedback" th:if="${#fields.hasErrors('confirmPassword')}"
|
||||||
|
th:errors="*{confirmPassword}">Error</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="d-flex justify-content-end">
|
||||||
|
<button type="submit" class="btn btn-secondary" th:text="#{app.guardar}">Guardar</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</th:block>
|
||||||
|
|
||||||
|
<th:block layout:fragment="modal" />
|
||||||
|
<th:block th:replace="~{theme/partials/vendor-scripts :: scripts}" />
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
||||||
Reference in New Issue
Block a user