From 6b883ffab29109d330041789ec3f5d6084c18a0a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jaime=20Jim=C3=A9nez?= Date: Sun, 24 Aug 2025 21:02:42 +0200 Subject: [PATCH] trabajando en resumen --- .../erp/externalApi/skApiClient.java | 94 ++-- .../erp/presupuesto/Presupuesto.java | 137 +++++- .../presupuesto/PresupuestoController.java | 86 +++- .../erp/presupuesto/PresupuestoService.java | 195 ++++++++- .../classes/PresupuestadorItems.java | 2 +- src/main/resources/i18n/app_en.properties | 1 + src/main/resources/i18n/app_es.properties | 1 + .../resources/i18n/presupuesto_es.properties | 55 ++- .../static/assets/css/presupuestador.css | 278 +++++++++++- .../presupuestador/presupuestador.js | 406 +++++++++++++++--- .../presupuestador/service-option-card.js | 46 ++ .../presupuestador/tirada-price-card.js | 123 ++++++ .../presupuestador-items/_cubierta.html | 167 ++++++- .../_datos-generales.html | 2 +- .../presupuestador-items/_extras.html | 31 ++ .../_seleccion-tirada.html | 41 ++ .../presupuestador-items/_summary.html | 74 ++++ .../presupuestos/presupuestador.html | 146 ++----- 18 files changed, 1659 insertions(+), 226 deletions(-) create mode 100644 src/main/resources/i18n/app_en.properties create mode 100644 src/main/resources/i18n/app_es.properties create mode 100644 src/main/resources/static/assets/js/pages/imprimelibros/presupuestador/service-option-card.js create mode 100644 src/main/resources/static/assets/js/pages/imprimelibros/presupuestador/tirada-price-card.js create mode 100644 src/main/resources/templates/imprimelibros/presupuestos/presupuestador-items/_extras.html create mode 100644 src/main/resources/templates/imprimelibros/presupuestos/presupuestador-items/_seleccion-tirada.html create mode 100644 src/main/resources/templates/imprimelibros/presupuestos/presupuestador-items/_summary.html diff --git a/src/main/java/com/imprimelibros/erp/externalApi/skApiClient.java b/src/main/java/com/imprimelibros/erp/externalApi/skApiClient.java index 03b9652..ecd259b 100644 --- a/src/main/java/com/imprimelibros/erp/externalApi/skApiClient.java +++ b/src/main/java/com/imprimelibros/erp/externalApi/skApiClient.java @@ -13,7 +13,6 @@ import com.fasterxml.jackson.databind.ObjectMapper; import java.util.Map; import java.util.HashMap; import java.util.function.Supplier; -import java.util.List; @Service public class skApiClient { @@ -30,7 +29,6 @@ public class skApiClient { } public String getPrice(Map requestBody) { - return performWithRetry(() -> { String url = this.skApiUrl + "api/calcular"; @@ -46,35 +44,43 @@ public class skApiClient { entity, String.class); - return response.getBody(); + try { + Map responseBody = new ObjectMapper().readValue(response.getBody(), Map.class); + + if (responseBody.get("error") == null) { + return new ObjectMapper().writeValueAsString( + Map.of("data", responseBody.get("data"))); + } else { + return "{\"error\": 1}"; + } + } catch (JsonProcessingException e) { + e.printStackTrace(); + return "{\"error\": 1}"; + } }); } public Integer getMaxSolapas(Map requestBody) { - - HttpHeaders headers = new HttpHeaders(); - headers.setContentType(MediaType.APPLICATION_JSON); - headers.setBearerAuth(authService.getToken()); - - Map request = new HashMap<>(requestBody); - Map data = new HashMap<>(); - try { String jsonResponse = performWithRetry(() -> { - String url = this.skApiUrl + "api/calcular-solapas"; - data.put("clienteId", request.get("clienteId")); - data.put("tamanio", (Map) request.get("tamanio")); + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_JSON); + headers.setBearerAuth(authService.getToken()); // token actualizado + + Map data = new HashMap<>(); + data.put("clienteId", requestBody.get("clienteId")); + data.put("tamanio", requestBody.get("tamanio")); data.put("tirada", requestBody.get("tirada")); - data.put("paginas", request.get("paginas")); - data.put("paginasColor", request.get("paginasColor")); + data.put("paginas", requestBody.get("paginas")); + data.put("paginasColor", requestBody.get("paginasColor")); data.put("papelInteriorDiferente", 0); - data.put("paginasCuadernillo", request.get("paginasCuadernillo")); - data.put("tipo", request.get("tipo")); - data.put("isColor", request.get("isColor")); - data.put("isHq", request.get("isHq")); - data.put("interior", request.get("interior")); + data.put("paginasCuadernillo", requestBody.get("paginasCuadernillo")); + data.put("tipo", requestBody.get("tipo")); + data.put("isColor", requestBody.get("isColor")); + data.put("isHq", requestBody.get("isHq")); + data.put("interior", requestBody.get("interior")); HttpEntity> entity = new HttpEntity<>(data, headers); @@ -88,19 +94,19 @@ public class skApiClient { }); ObjectMapper mapper = new ObjectMapper(); - JsonNode root = mapper.readTree(jsonResponse); + if (root.get("data") == null || !root.get("data").isInt()) { throw new RuntimeException("Respuesta inesperada de calcular-solapas: " + jsonResponse); } - int solapas = root.get("data").asInt(); - return solapas; + + return root.get("data").asInt(); + } catch (JsonProcessingException e) { - // No se puede calcular el interior, por lo que las solapas seran el 80% del - // ancho - Map tamanio = (Map)data.get("tamanio"); + // Fallback al 80% del ancho + Map tamanio = (Map) requestBody.get("tamanio"); if (tamanio == null || tamanio.get("ancho") == null) - throw new RuntimeException("Tamaño no válido en la solicitud: " + data); + throw new RuntimeException("Tamaño no válido en la solicitud: " + requestBody); else { int ancho = (int) tamanio.get("ancho"); return (int) Math.floor(ancho * 0.8); // 80% del ancho @@ -108,6 +114,38 @@ public class skApiClient { } } + public Double getRetractilado(Map requestBody) { + + String value = performWithRetry(() -> { + String url = this.skApiUrl + "api/calcular-retractilado"; + + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_JSON); + headers.setBearerAuth(authService.getToken()); + + HttpEntity> entity = new HttpEntity<>(requestBody, headers); + + ResponseEntity response = restTemplate.exchange( + url, + HttpMethod.POST, + entity, + String.class); + + try { + Map responseBody = new ObjectMapper().readValue(response.getBody(), Map.class); + return responseBody.get("data").toString(); + } catch (JsonProcessingException e) { + e.printStackTrace(); + return "0.0"; // Fallback en caso de error + } + }); + try { + return Double.parseDouble(value); + } catch (NumberFormatException e) { + throw new RuntimeException("Error al parsear el valor de retractilado: " + value, e); + } + } + /****************** * PRIVATE METHODS ******************/ diff --git a/src/main/java/com/imprimelibros/erp/presupuesto/Presupuesto.java b/src/main/java/com/imprimelibros/erp/presupuesto/Presupuesto.java index 7aa11f9..19aa0b8 100644 --- a/src/main/java/com/imprimelibros/erp/presupuesto/Presupuesto.java +++ b/src/main/java/com/imprimelibros/erp/presupuesto/Presupuesto.java @@ -14,7 +14,7 @@ import jakarta.persistence.*; @Tamanio(groups = PresupuestoValidationGroups.DatosGenerales.class) @Entity @Table(name = "presupuesto") -public class Presupuesto { +public class Presupuesto implements Cloneable{ public enum TipoEncuadernacion { fresado, cosido, grapado, espiral, wireo @@ -28,6 +28,15 @@ public class Presupuesto { tapaBlanda, tapaDura, tapaDuraLomoRedondo } + @Override + public Presupuesto clone() { + try { + return (Presupuesto) super.clone(); + } catch (CloneNotSupportedException e) { + throw new RuntimeException(e); + } + } + @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @@ -59,6 +68,9 @@ public class Presupuesto { @Column(name = "tirada4") private Integer tirada4; + @Column(name = "selected_tirada") + private Integer selectedTirada; + @NotNull(message = "{presupuesto.errores.ancho}", groups = PresupuestoValidationGroups.DatosGenerales.class) @Column(name = "ancho") private Integer ancho; @@ -134,10 +146,32 @@ public class Presupuesto { @NotNull(message = "{presupuesto.errores.acabado-cubierta}", groups = PresupuestoValidationGroups.Cubierta.class) @Column(name = "acabado") private Integer acabado = 1; - - // Getters y Setters + @Column(name = "sobrecubierta") + private Boolean sobrecubierta = false; + @Column(name = "papel_sobrecubierta_id") + private Integer papelSobrecubiertaId = 2; + @Column(name = "gramaje_sobrecubierta") + private Integer gramajeSobrecubierta = 170; + @Column(name = "tamanio_solapas_sobrecubierta") + private Integer tamanioSolapasSobrecubierta = 80; + @Column(name = "acabado_sobrecubierta") + private Integer acabadoSobrecubierta = 0; // 0: sin acabado, + @Column(name = "faja") + private Boolean faja = false; + @Column(name = "papel_faja_id") + private Integer papelFajaId = 2; + @Column(name = "gramaje_faja") + private Integer gramajeFaja = 170; + @Column(name = "tamanio_solapas_faja") + private Integer tamanioSolapasFaja = 80; + @Column(name = "acabado_faja") + private Integer acabadoFaja = 0; // 0: sin acabado + @Column(name = "alto_faja") + private Integer altoFaja = 0; + + // Getters y Setters public String getAutor() { return autor; } @@ -382,4 +416,101 @@ public class Presupuesto { this.acabado = acabado; } + public Boolean getSobrecubierta() { + return sobrecubierta; + } + + public void setSobrecubierta(Boolean sobrecubierta) { + this.sobrecubierta = sobrecubierta; + } + + public Integer getPapelSobrecubiertaId() { + return papelSobrecubiertaId; + } + + public void setPapelSobrecubiertaId(Integer papelSobrecubiertaId) { + this.papelSobrecubiertaId = papelSobrecubiertaId; + } + + public Integer getGramajeSobrecubierta() { + return gramajeSobrecubierta; + } + + public void setGramajeSobrecubierta(Integer gramajeSobrecubierta) { + this.gramajeSobrecubierta = gramajeSobrecubierta; + } + + public Integer getTamanioSolapasSobrecubierta() { + return tamanioSolapasSobrecubierta; + } + + public void setTamanioSolapasSobrecubierta(Integer tamanioSolapasSobrecubierta) { + this.tamanioSolapasSobrecubierta = tamanioSolapasSobrecubierta; + } + + public Integer getAcabadoSobrecubierta() { + return acabadoSobrecubierta; + } + + public void setAcabadoSobrecubierta(Integer acabadoSobrecubierta) { + this.acabadoSobrecubierta = acabadoSobrecubierta; + } + + public Boolean getFaja() { + return faja; + } + + public void setFaja(Boolean faja) { + this.faja = faja; + } + + public Integer getPapelFajaId() { + return papelFajaId; + } + + public void setPapelFajaId(Integer papelFajaId) { + this.papelFajaId = papelFajaId; + } + + public Integer getGramajeFaja() { + return gramajeFaja; + } + + public void setGramajeFaja(Integer gramajeFaja) { + this.gramajeFaja = gramajeFaja; + } + + public Integer getTamanioSolapasFaja() { + return tamanioSolapasFaja; + } + + public void setTamanioSolapasFaja(Integer tamanioSolapasFaja) { + this.tamanioSolapasFaja = tamanioSolapasFaja; + } + + public Integer getAcabadoFaja() { + return acabadoFaja; + } + + public void setAcabadoFaja(Integer acabadoFaja) { + this.acabadoFaja = acabadoFaja; + } + + public Integer getAltoFaja() { + return altoFaja; + } + + public void setAltoFaja(Integer altoFaja) { + this.altoFaja = altoFaja; + } + + public Integer getSelectedTirada() { + return selectedTirada; + } + + public void setSelectedTirada(Integer selectedTirada) { + this.selectedTirada = selectedTirada; + } + + } diff --git a/src/main/java/com/imprimelibros/erp/presupuesto/PresupuestoController.java b/src/main/java/com/imprimelibros/erp/presupuesto/PresupuestoController.java index 7051055..ae2888b 100644 --- a/src/main/java/com/imprimelibros/erp/presupuesto/PresupuestoController.java +++ b/src/main/java/com/imprimelibros/erp/presupuesto/PresupuestoController.java @@ -6,16 +6,18 @@ import java.util.HashMap; import java.util.Locale; import java.util.Map; import java.util.List; -import java.util.Collections; import org.springframework.beans.factory.annotation.Autowired; - +import org.springframework.context.MessageSource; import org.springframework.http.ResponseEntity; import org.springframework.validation.BindingResult; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.PostMapping; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.core.type.TypeReference; import com.imprimelibros.erp.externalApi.skApiClient; import com.imprimelibros.erp.presupuesto.classes.ImagenPresupuesto; import com.imprimelibros.erp.presupuesto.validation.PresupuestoValidationGroups; @@ -30,6 +32,9 @@ public class PresupuestoController { @Autowired protected skApiClient apiClient; + @Autowired + protected MessageSource messageSource; + @PostMapping("/public/validar/datos-generales") public ResponseEntity validarDatosGenerales( @Validated(PresupuestoValidationGroups.DatosGenerales.class) Presupuesto presupuesto, @@ -80,6 +85,63 @@ public class PresupuestoController { return ResponseEntity.ok(resultado); } + @PostMapping("/public/validar/cubierta") + public ResponseEntity validarCubierta( + @Validated(PresupuestoValidationGroups.Cubierta.class) Presupuesto presupuesto, + BindingResult result, Locale locale) { + + Map errores = new HashMap<>(); + + // errores de campos individuales + result.getFieldErrors().forEach(error -> errores.put(error.getField(), error.getDefaultMessage())); + + // errores globales (@ConsistentTiradas...) + result.getGlobalErrors().forEach(error -> errores.put("global", error.getDefaultMessage())); + + if (!errores.isEmpty()) { + return ResponseEntity.badRequest().body(errores); + } + + HashMap price = new HashMap<>(); + String priceStr = apiClient.getPrice(presupuestoService.toSkApiRequest(presupuesto)); + + try { + price = new ObjectMapper().readValue(priceStr, new TypeReference<>() { + }); + } catch (JsonProcessingException e) { + price = new HashMap<>(); + price.put("error", messageSource.getMessage("presupuesto.error-obtener-precio", null, locale)); + } + if (!price.containsKey("data")) { + return ResponseEntity.badRequest().body(messageSource.getMessage("presupuesto.error-obtener-precio", null, locale)); + } + return ResponseEntity.ok(price.get("data")); + } + + @PostMapping("/public/validar/seleccion-tirada") + public ResponseEntity validarSeleccionTirada( + Presupuesto presupuesto, + BindingResult result, Locale locale) { + + Map errores = new HashMap<>(); + + // errores de campos individuales + result.getFieldErrors().forEach(error -> errores.put(error.getField(), error.getDefaultMessage())); + + // errores globales (@ConsistentTiradas...) + result.getGlobalErrors().forEach(error -> errores.put("global", error.getDefaultMessage())); + + if (!errores.isEmpty()) { + return ResponseEntity.badRequest().body(errores); + } + + Map resultado = new HashMap<>(); + // servicios extra + resultado.putAll(presupuestoService.obtenerServiciosExtras(presupuesto, locale, apiClient)); + + return ResponseEntity.ok(resultado); + } + @PostMapping("/public/get-papel-interior") public ResponseEntity getPapelInterior( @Validated(PresupuestoValidationGroups.Interior.class) Presupuesto presupuesto, @@ -212,4 +274,24 @@ public class PresupuestoController { return ResponseEntity.ok(resultado); } + @PostMapping("/public/get-price") + public ResponseEntity getPrice( + Presupuesto presupuesto, + BindingResult result, Locale locale) { + + Map errores = new HashMap<>(); + // errores de campos individuales + result.getFieldErrors().forEach(error -> errores.put(error.getField(), error.getDefaultMessage())); + // errores globales (@ConsistentTiradas...) + result.getGlobalErrors().forEach(error -> errores.put("global", error.getDefaultMessage())); + if (!errores.isEmpty()) { + return ResponseEntity.badRequest().body(errores); + } + String price = apiClient.getPrice(presupuestoService.toSkApiRequest(presupuesto)); + if (price == null || price.isEmpty()) { + return ResponseEntity.badRequest().body("No se pudo obtener el precio. Intente nuevamente."); + } + return ResponseEntity.ok(price); + + } } diff --git a/src/main/java/com/imprimelibros/erp/presupuesto/PresupuestoService.java b/src/main/java/com/imprimelibros/erp/presupuesto/PresupuestoService.java index 6007948..ae18f25 100644 --- a/src/main/java/com/imprimelibros/erp/presupuesto/PresupuestoService.java +++ b/src/main/java/com/imprimelibros/erp/presupuesto/PresupuestoService.java @@ -8,15 +8,21 @@ import java.util.Map; import java.util.Objects; import java.util.stream.Collectors; import java.util.Locale; +import java.text.NumberFormat; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.MessageSource; +import org.springframework.http.ResponseEntity; import org.springframework.stereotype.Service; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; import com.imprimelibros.erp.configurationERP.VariableService; import com.imprimelibros.erp.presupuesto.Presupuesto.TipoCubierta; import com.imprimelibros.erp.presupuesto.classes.ImagenPresupuesto; import com.imprimelibros.erp.presupuesto.classes.PresupuestadorItems; +import com.imprimelibros.erp.externalApi.skApiClient; @Service public class PresupuestoService { @@ -27,6 +33,9 @@ public class PresupuestoService { @Autowired protected MessageSource messageSource; + @Autowired + protected skApiClient skApiClient; + private final PresupuestadorItems presupuestadorItems; public PresupuestoService(PresupuestadorItems presupuestadorItems) { @@ -228,7 +237,7 @@ public class PresupuestoService { List gramajes = new ArrayList<>(); - final int CARTULINA_GRAFICA_ID = 3; + final int CARTULINA_GRAFICA_ID = 5; final int ESTUCADO_MATE_ID = 2; if (presupuesto.getPapelCubiertaId() != null && presupuesto.getPapelCubiertaId() == CARTULINA_GRAFICA_ID) { @@ -269,17 +278,11 @@ public class PresupuestoService { "gramajeCubierta", presupuesto.getGramajeCubierta(), "carasImpresion", presupuesto.getCubiertaCaras(), "solapas", presupuesto.getSolapasCubierta() ? presupuesto.getTamanioSolapasCubierta() : 0, - "acabado", 0, //// Añadir acabados + "acabado", presupuesto.getAcabado(), "cabezada", presupuesto.getCabezada(), "lomoRedondo", presupuesto.getTipoCubierta() == TipoCubierta.tapaDuraLomoRedondo ? 1 : 0); /* - * Map sobrecubierta = new HashMap<>(); - * sobrecubierta.put("papel", "2"); - * sobrecubierta.put("gramaje", 170); - * sobrecubierta.put("solapas", 80); - * sobrecubierta.put("acabado", null); - * * Map servicios = Map.of( * "retractilado", 0, * "retractilado5", 0, @@ -303,9 +306,25 @@ public class PresupuestoService { body.put("paginasCuadernillo", SK_PAGINAS_CUADERNILLO); body.put("interior", interior); body.put("cubierta", cubierta); - // body.put("sobrecubierta", sobrecubierta); body.put("guardas", null); - body.put("faja", false); + if (presupuesto.getSobrecubierta()) { + Map sobrecubierta = new HashMap<>(); + sobrecubierta.put("papel", presupuesto.getPapelSobrecubiertaId()); + sobrecubierta.put("gramaje", presupuesto.getGramajeSobrecubierta()); + sobrecubierta.put("solapas", presupuesto.getTamanioSolapasSobrecubierta()); + sobrecubierta.put("acabado", presupuesto.getAcabadoSobrecubierta()); + body.put("sobrecubierta", sobrecubierta); + } + + if (presupuesto.getFaja()) { + Map faja = new HashMap<>(); + faja.put("papel", presupuesto.getPapelFajaId()); + faja.put("gramaje", presupuesto.getGramajeFaja()); + faja.put("solapas", presupuesto.getTamanioSolapasFaja()); + faja.put("acabado", presupuesto.getAcabadoFaja()); + faja.put("alto", presupuesto.getAltoFaja()); + body.put("faja", faja); + } // body.put("servicios", servicios); return body; @@ -372,7 +391,8 @@ public class PresupuestoService { }); opciones.add(new HashMap<>() { { - put("name", messageSource.getMessage("presupuesto.acabado-plastificado-mate-1c-antirrayado", null, locale)); + put("name", + messageSource.getMessage("presupuesto.acabado-plastificado-mate-1c-antirrayado", null, locale)); put("sk-id", 8); } }); @@ -405,4 +425,157 @@ public class PresupuestoService { return resultado; } + public Map aplicarMargenTiradas(Map data) { + + // implementar margenes + return (Map) data; + } + + public String obtenerPrecioRetractilado(Presupuesto presupuesto, Locale locale) { + Integer[] tiradas = presupuesto.getTiradas(); + Integer tirada_min = Arrays.stream(tiradas) + .filter(Objects::nonNull) + .min(Integer::compareTo) + .orElse(0); + Map requestBody = new HashMap<>(); + requestBody.put("tirada", + presupuesto.getSelectedTirada() != null ? presupuesto.getSelectedTirada() : tirada_min); + Double precio_retractilado = skApiClient.getRetractilado(requestBody); + return precio_retractilado != null + ? NumberFormat.getNumberInstance(locale) + .format(Math.round(precio_retractilado * 100.0) / 100.0) + : "0,00"; + } + + public Map obtenerServiciosExtras(Presupuesto presupuesto, Locale locale, skApiClient apiClient) { + + List opciones = new ArrayList<>(); + + Double price_prototipo = this.obtenerPrototipo(presupuesto, apiClient); + + opciones.add(new HashMap() { + { + put("id", "retractilado"); + put("title", messageSource.getMessage("presupuesto.extras-retractilado", null, locale)); + put("description", ""); + put("price", obtenerPrecioRetractilado(presupuesto, locale)); + put("priceUnit", messageSource.getMessage("app.currency-symbol", null, locale)); + } + }); + opciones.add(new HashMap() { + { + put("id", "service-isbn"); + put("title", messageSource.getMessage("presupuesto.extras-isbn", null, locale)); + put("description", ""); + put("price", "30"); + put("priceUnit", messageSource.getMessage("app.currency-symbol", null, locale)); + } + }); + opciones.add(new HashMap() { + { + put("id", "deposito-legal"); + put("title", messageSource.getMessage("presupuesto.extras-deposito-legal", null, locale)); + put("description", + messageSource.getMessage("presupuesto.extras-deposito-legal-descripcion", null, locale)); + put("price", "30"); + put("priceUnit", messageSource.getMessage("app.currency-symbol", null, locale)); + } + }); + opciones.add(new HashMap() { + { + put("id", "revision-archivos"); + put("title", messageSource.getMessage("presupuesto.extras-revision-archivos", null, locale)); + put("description", ""); + put("price", "50"); + put("priceUnit", messageSource.getMessage("app.currency-symbol", null, locale)); + } + }); + opciones.add(new HashMap() { + { + put("id", "maquetacion-cubierta"); + put("title", messageSource.getMessage("presupuesto.extras-maquetacion-cubierta", null, locale)); + put("description", ""); + put("price", "50"); + put("priceUnit", messageSource.getMessage("app.currency-symbol", null, locale)); + } + }); + opciones.add(new HashMap() { + { + put("id", "ferro-digital"); + put("title", messageSource.getMessage("presupuesto.extras-ferro-digital", null, locale)); + put("description", ""); + put("price", "0"); + put("priceUnit", messageSource.getMessage("app.currency-symbol", null, locale)); + put("checked", "true"); + put("allowChange", "false"); + put("ribbonText", messageSource.getMessage("presupuesto.extras-ferro-digital-ribbon", null, locale)); + } + }); + opciones.add(new HashMap() { + { + put("id", "ejemplar-prueba"); + put("title", messageSource.getMessage("presupuesto.extras-ejemplar-prueba", null, locale)); + put("description", ""); + if(price_prototipo == 0.0) { + put("price", messageSource.getMessage("presupuesto.consultar-soporte", null, locale)); + put("priceUnit", ""); + } else { + put("price", NumberFormat.getNumberInstance(locale) + .format(Math.round(price_prototipo * 100.0) / 100.0)); + put("priceUnit", messageSource.getMessage("app.currency-symbol", null, locale)); + } + } + }); + opciones.add(new HashMap() { + { + put("id", "marcapaginas"); + put("title", messageSource.getMessage("presupuesto.extras-marcapaginas", null, locale)); + put("description", ""); + put("price", messageSource.getMessage("presupuesto.extras-calcular", null, locale)); + } + }); + opciones.add(new HashMap() { + { + put("id", "maquetacion"); + put("title", messageSource.getMessage("presupuesto.extras-maquetacion", null, locale)); + put("description", ""); + put("price", messageSource.getMessage("presupuesto.extras-calcular", null, locale)); + } + }); + + Map response = new HashMap<>(); + response.put("servicios_extra", opciones); + return response; + } + + private Double obtenerPrototipo(Presupuesto presupuesto, skApiClient apiClient) { + + // Obtenemos el precio de 1 unidad para el ejemplar de prueba + HashMap price = new HashMap<>(); + // make a copy of "presupuesto" to avoid modifying the original object + Presupuesto presupuestoTemp = presupuesto.clone(); + presupuestoTemp.setTirada1(1); + presupuestoTemp.setTirada2(null); + presupuestoTemp.setTirada3(null); + presupuestoTemp.setTirada4(null); + if (presupuestoTemp.getTipoImpresion() == Presupuesto.TipoImpresion.color) { + presupuestoTemp.setTipoImpresion(Presupuesto.TipoImpresion.colorhq); + } else if (presupuestoTemp.getTipoImpresion() == Presupuesto.TipoImpresion.negro) { + presupuestoTemp.setTipoImpresion(Presupuesto.TipoImpresion.negrohq); + } + String priceStr = apiClient.getPrice(this.toSkApiRequest(presupuestoTemp)); + Double price_prototipo = 0.0; + try { + price = new ObjectMapper().readValue(priceStr, new TypeReference<>() { + }); + price_prototipo = ((List) ((Map) price.get("data")).get("precios")).get(0); + if (price_prototipo < 25) { + price_prototipo = 25.0; + } + + } catch (JsonProcessingException e) { + } catch (Exception exception) { + } + return price_prototipo; + } } diff --git a/src/main/java/com/imprimelibros/erp/presupuesto/classes/PresupuestadorItems.java b/src/main/java/com/imprimelibros/erp/presupuesto/classes/PresupuestadorItems.java index 40996b4..83c5bbe 100644 --- a/src/main/java/com/imprimelibros/erp/presupuesto/classes/PresupuestadorItems.java +++ b/src/main/java/com/imprimelibros/erp/presupuesto/classes/PresupuestadorItems.java @@ -112,7 +112,7 @@ public class PresupuestadorItems { "/assets/images/imprimelibros/presupuestador/cartulina-grafica.png", "", messageSource.getMessage("presupuesto.cartulina-grafica-cubierta", null, locale), - Map.of("sk-id", "3"), + Map.of("sk-id", "5"), false); } diff --git a/src/main/resources/i18n/app_en.properties b/src/main/resources/i18n/app_en.properties new file mode 100644 index 0000000..b2b53d4 --- /dev/null +++ b/src/main/resources/i18n/app_en.properties @@ -0,0 +1 @@ +app.currency-symbol=€ \ No newline at end of file diff --git a/src/main/resources/i18n/app_es.properties b/src/main/resources/i18n/app_es.properties new file mode 100644 index 0000000..b2b53d4 --- /dev/null +++ b/src/main/resources/i18n/app_es.properties @@ -0,0 +1 @@ +app.currency-symbol=€ \ No newline at end of file diff --git a/src/main/resources/i18n/presupuesto_es.properties b/src/main/resources/i18n/presupuesto_es.properties index 39d1c98..ae7202e 100644 --- a/src/main/resources/i18n/presupuesto_es.properties +++ b/src/main/resources/i18n/presupuesto_es.properties @@ -1,6 +1,7 @@ presupuesto.datos-generales=Datos Generales presupuesto.interior=Interior presupuesto.cubierta=Cubierta +presupuesto.seleccion-tirada=Seleccion de tirada presupuesto.extras=Extras # Pestaña datos generales de presupuesto @@ -85,9 +86,12 @@ presupuesto.estucado-mate-cubierta=Estucado mate presupuesto.gramaje-cubierta=Gramaje cubierta presupuesto.gramaje-cubierta-descripcion=Seleccione el gramaje para la cubierta presupuesto.volver-interior=Volver a diseño interior -presupuesto.continuar-extras-libro=Continuar a extras del libro +presupuesto.continuar-seleccion-tirada=Continuar a selección de tirada presupuesto.offset=Offset presupuesto.estucado=Estucado +presupuesto.verjurado=Verjurado +presupuesto.verjurado-blanco-natural=Verjurado blanco natural +presupuesto.verjurado-ahuesado=Verjurado ahuesado presupuesto.acabado=Acabado presupuesto.acabado-cubierta-descripcion=Seleccione el acabado para la cubierta presupuesto.acabado-cubierta-aviso=La falta de plastificado en la cubierta puede comprometer su calidad, ya que aumenta el riesgo de agrietamiento en los pliegues o hendidos, afectando su apariencia y resistencia. @@ -99,6 +103,55 @@ presupuesto.acabado-plastificado-mate-uvi=Plastificado Mate 1/C + Reserva UVI presupuesto.acabado-plastificado-mate-uvi3d=Plastificado Mate 1/C + Reserva UVI 3D presupuesto.acabado-plastificado-mate-uvi-braile=Plastificado Mate 1/C + Reserva UVI Braille presupuesto.acabado-plastificado-sandy-1c=Plastificado Sandy 1/C (tacto arena) +presupuesto.cubierta-extras=Extras de cubierta +presupuesto.cubierta-extras-descripcion=Seleccione las opciones adicionales para la cubierta +presupuesto.sobrecubierta=Sobrecubierta +presupuesto.sobrecubierta-papel=Papel sobrecubierta +presupuesto.sobrecubierta-solapas=Tamaño solapas sobrecubierta +presupuesto.faja=Faja +presupuesto.faja-papel=Papel faja +presupuesto.faja-solapas=Tamaño solapas faja +presupuesto.faja-alto=Alto faja + +#pestaña seleccion-tirada +presupuesto.seleccion-tirada-descripcion=Seleccione la tirada deseada +presupuesto.total=Total +presupuesto.precio-unidad=Precio por unidad +presupuesto.seleccionar-tirada=Seleccionar tirada +presupuesto.tirada-seleccionada=Seleccionada +presupuesto.unidades=UNIDADES +presupuesto.volver-seleccion-tirada=Volver a selección de tirada +presupuesto.continuar-extras-libro=Continuar a extras del libro +presupuesto.error-obtener-precio=No se pudo obtener el precio para los datos introducidos. Por favor, contacte con el soporte técnico. + +#pestaña extras del libro +presupuesto.extras=Servicios Extras +presupuesto.extras-descripcion=Seleccione los servicios adicionales que desea añadir al presupuesto +presupuesto.extras-retractilado=Retractilado +presupuesto.extras-isbn=ISBN +presupuesto.extras-deposito-legal=Depósito Legal +presupuesto.extras-deposito-legal-descripcion=Se añadirán 4 ejemplares a la tirada +presupuesto.extras-revision-archivos=Revisión de archivos +presupuesto.extras-maquetacion-cubierta=Maquetación de cubierta +presupuesto.extras-ferro-digital=Ferro Digital +presupuesto.extras-ejemplar-prueba=Ejemplar de prueba +presupuesto.extras-marcapaginas=Marcapáginas +presupuesto.extras-maquetacion=Maquetación +presupuesto.extras-ferro-digital-ribbon=Incluido +presupuesto.extras-calcular=Calcular +presupuesto.volver-cubierta=Volver a diseño cubierta +presupuesto.finalizar=Finalizar presupuesto +presupuesto.calcular-presupuesto=Calcular presupuesto +presupuesto.consultar-soporte=Consultar con soporte + +# Resumen del presupuesto +presupuesto.resumen-presupuesto=Resumen presupuesto +presupuesto.resumen-libro=Libro +presupuesto.resumen-encuadernacion=Encuadernación +presupuesto.interior-libro=Interior del libro +presupuesto.cubierta-libro=Cubierta del libro +presupuesto.extras-libro=Extras del libro +presupuesto.paginas=Páginas # Errores presupuesto.errores-title=Corrija los siguientes errores: diff --git a/src/main/resources/static/assets/css/presupuestador.css b/src/main/resources/static/assets/css/presupuestador.css index 3db239a..940119b 100644 --- a/src/main/resources/static/assets/css/presupuestador.css +++ b/src/main/resources/static/assets/css/presupuestador.css @@ -7,8 +7,8 @@ text-align: center; /* Tamaño adaptable */ - width: 100%; - max-width: 200px; + width: 100%; + max-width: 200px; /* Para evitar que la imagen sobresalga al hacer zoom */ overflow: hidden; @@ -30,7 +30,7 @@ /* === Imagen interna === */ .image-container img { - max-width: 100%; + max-width: 100%; max-height: 150px; display: block; transform-origin: center center; @@ -44,10 +44,21 @@ /* Keyframes para la animación */ @keyframes zoomPop { - 0% { transform: scale(1); } - 40% { transform: scale(0.85); } - 80% { transform: scale(1.05); } - 100% { transform: scale(1); } + 0% { + transform: scale(1); + } + + 40% { + transform: scale(0.85); + } + + 80% { + transform: scale(1.05); + } + + 100% { + transform: scale(1); + } } .image-container:hover { @@ -61,15 +72,19 @@ } } -.gramaje-radio{ +.gramaje-radio { display: flex; flex-direction: column; align-items: center; justify-content: center; - min-width: 70px; /* Ancho mínimo */ - min-height: 70px; /* Alto mínimo */ - max-width: 70px; /* Ancho máximo */ - max-height: 70px; /* Alto máximo */ + min-width: 70px; + /* Ancho mínimo */ + min-height: 70px; + /* Alto mínimo */ + max-width: 70px; + /* Ancho máximo */ + max-height: 70px; + /* Alto máximo */ } @keyframes fadeInUpBounce { @@ -77,13 +92,16 @@ opacity: 0; transform: translateY(30px); } + 60% { opacity: 1; transform: translateY(-10px); } + 80% { transform: translateY(5px); } + 100% { transform: translateY(0); } @@ -93,4 +111,240 @@ animation: fadeInUpBounce 0.6s ease-out both; animation-delay: 0.1s; will-change: transform; +} + +.service-option { + cursor: pointer; + transition: all 0.3s ease-in-out; + border: 1px solid #d0d7e5; + border-radius: 0.5rem; + background-color: #fff; +} + +.service-option.checked { + border-color: #4b7bec; + background-color: #f0f5ff; + box-shadow: 0 0 0 2px #4b7bec inset; +} + +.service-option .price { + font-weight: 600; + font-size: 1rem; + margin-top: 1rem; +} + +.service-desc { + min-height: 2.2rem; + /* ajusta según el tamaño de fuente que uses */ + display: flex; + align-items: center; + justify-content: center; +} + +.service-checkbox { + display: none; +} + +.btn-check-service+.btn { + position: relative; + /* <-- Esto es lo más importante */ + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + white-space: normal; + line-height: 1.2; + min-height: 180px; + padding: 1rem 0.75rem; + border-radius: 0.5rem; + color: #4b7bec; + border: 1px solid #4b7bec; + transition: all 0.3s ease-in-out; +} + +/* ESTILO CUANDO ESTÁ MARCADO */ +.btn-check-service:checked+.btn { + background-color: #4b7bec; + color: #ffffff !important; + border-color: #4b7bec; +} + +/* Forzamos el color blanco en los subelementos */ +.btn-check-service:checked+.btn .service-title, +.btn-check-service:checked+.btn .service-desc, +.btn-check-service:checked+.btn .service-price { + color: #ffffff !important; +} + +/* Forzamos el color azul cuando no está marcado */ +.btn-check-service+.btn .service-title, +.btn-check-service+.btn .service-desc, +.btn-check-service+.btn .service-price { + color: #4b7bec; +} + +/* ribbon-service */ +.ribbon-service { + position: absolute; + top: -5px; + right: -5px; + overflow: hidden; + width: 90px; + height: 90px; + z-index: 1; +} + +.ribbon-service span { + position: absolute; + display: block; + width: 120px; + padding: 5px 0; + background: #f25c5c; + color: white; + font-size: 12px; + text-align: center; + font-weight: bold; + transform: rotate(45deg); + top: 20px; + right: -30px; + box-shadow: 0 2px 6px rgba(0, 0, 0, 0.2); +} + + +/* ===== Tiradas (pricing cards) ===== */ +.tirada-card { + --il-accent: #4b7bec; + --radius: 18px; + --sel-scale-y: 1.12; + /* cuánto más alta la seleccionada (crece arriba y abajo) */ + + /* círculo */ + --arc-w: 280px; + --arc-h: 190px; + --arc-y: -23px; + /* tu valor */ + --arc-stop: 74%; + + border: 1px solid #e9ecef; + border-radius: var(--radius); + background-color: #fff; + background-image: radial-gradient(var(--arc-w) var(--arc-h) at 50% var(--arc-y), + rgba(75, 126, 236, .24) 0 var(--arc-stop), + transparent calc(var(--arc-stop) + 1%)); + + padding: 1.25rem 1.25rem 1rem; + text-align: center; + position: relative; + height: 100%; + box-shadow: 0 4px 14px rgba(17, 24, 39, .06); + overflow: hidden; + /* integra la línea inferior */ + transform-origin: center center; + /* ⟵ crecer hacia arriba y hacia abajo */ + will-change: transform; + transition: transform .22s ease, box-shadow .22s ease, border-color .22s ease, background .22s ease; +} + +/* sin elevación al hover */ +.tirada-card:hover { + transform: none; +} + +/* Tipografías */ +.tirada-card .title { + font-weight: 700; + letter-spacing: .04em; + font-size: .9rem; + color: #4b7bec; + text-transform: uppercase; +} + +.tirada-card .price-big { + font-size: 2rem; + font-weight: 800; + line-height: 1.1; + margin: .35rem 0 .15rem; + color: #4b7bec; +} + +.tirada-card .per { + font-size: .85rem; + color: #7f8fa9; +} + +.tirada-card .price-row { + margin-top: .75rem; +} + +.tirada-card .muted { + color: #8e9bb3; + font-size: .9rem; +} + +/* Botón */ +.tirada-card .btn-select-tirada { + margin-top: 1rem; + border-radius: 999px; + padding: .6rem 1rem; + font-weight: 600; + border: 2px solid var(--il-accent); + color: var(--il-accent); + background: transparent; + width: 100%; + transition: background .2s ease, color .2s ease; +} + +/* ===== Seleccionada (más alta en Y, sin elevar; crece arriba y abajo) ===== */ +.tirada-card.selected { + transform: scaleY(var(--sel-scale-y)); + /* ⟵ crecimiento simétrico vertical */ + z-index: 2; + box-shadow: 0 18px 48px rgba(75, 126, 236, .32); + border-color: var(--il-accent); + + /* círculo sólido */ + background-image: radial-gradient(var(--arc-w) var(--arc-h) at 50% var(--arc-y), + #4b7bec 0 var(--arc-stop), + transparent calc(var(--arc-stop) + 1%)); +} + +.tirada-card.selected:hover { + /* evita que :hover base anule la escala */ + transform: scaleY(var(--sel-scale-y)); +} + +.tirada-card.selected .btn-select-tirada { + background: var(--il-accent); + color: #fff; +} + +/* texto en blanco dentro del círculo */ +.tirada-card.selected .title, +.tirada-card.selected .price-big, +.tirada-card.selected .per { + color: #fff; +} + +/* ===== Línea inferior integrada (siempre visible en NO seleccionadas) ===== */ +.tirada-card:not(.selected)::after { + content: ""; + position: absolute; + left: 1px; + right: 1px; + bottom: 1px; + /* dentro del borde */ + height: 8px; + background: var(--il-accent); + border-bottom-left-radius: calc(var(--radius) - 1px); + border-bottom-right-radius: calc(var(--radius) - 1px); + pointer-events: none; +} + +.tirada-card.selected::after { + content: none; +} + +/* Oculta el radio */ +.tirada-card input[type="radio"] { + display: none; } \ No newline at end of file diff --git a/src/main/resources/static/assets/js/pages/imprimelibros/presupuestador/presupuestador.js b/src/main/resources/static/assets/js/pages/imprimelibros/presupuestador/presupuestador.js index 7cc6d9f..5da272e 100644 --- a/src/main/resources/static/assets/js/pages/imprimelibros/presupuestador/presupuestador.js +++ b/src/main/resources/static/assets/js/pages/imprimelibros/presupuestador/presupuestador.js @@ -1,4 +1,6 @@ import imagen_presupuesto from "./imagen-presupuesto.js"; +import ServiceOptionCard from "./service-option-card.js"; +import TiradaCard from "./tirada-price-card.js"; class PresupuestoCliente { @@ -39,8 +41,24 @@ class PresupuestoCliente { cabezada: 'WHI', papelCubiertaId: 3, gramajeCubierta: 170, - acabado: 1 - } + acabado: 1, + sobrecubierta: { + activo: false, + papelSobrecubiertaId: 2, + gramajeSobrecubierta: 170, + tamanioSolapasSobrecubierta: 80, + acabado: 0 + }, + faja: { + activo: false, + papelFajaId: 2, + gramajeFaja: 170, + alto: 50, + tamanioSolapasFaja: 80, + acabado: 0 + } + }, + selectedTirada: null, } // pestaña datos generales @@ -85,13 +103,44 @@ class PresupuestoCliente { this.guardasImpresas = $('#guardas-impresas'); this.cabezada = $('#cabezada'); this.divAcabadoCubierta = $('#div-acabado-cubierta'); + this.divSobrecubierta = $('#div-sobrecubierta'); + this.divFaja = $('#div-faja'); + this.acabadoSobrecubierta = $('#sobrecubierta-acabado'); + this.fajaSobrecubierta = $('#faja-acabado'); + this.sobrecubierta = $('#sobrecubierta'); + this.papelSobrecubierta = $('#papel-sobrecubierta'); + this.solapasSobrecubierta = $('#tamanio-solapas-sobrecubierta'); + this.faja = $('#faja'); + this.papelFaja = $('#papel-faja'); + this.altoFaja = $('#alto-faja'); + this.solapasFaja = $('#tamanio-solapas-faja'); + this.acabadoFaja = $('#faja-acabado'); + + // seleccion tirada + this.divTiradas = $('#div-tiradas'); + this.divTiradasError = $('#div-tiradas-error'); + + // pestaña extras + this.divExtras = $('#div-extras'); } init() { + const stored = sessionStorage.getItem("formData"); + this.#initDatosGenerales(); - this.#initInterior(); this.#initCubierta(); + + if (stored) { + this.formData = JSON.parse(stored); + this.#loadDatosGeneralesData(); + this.#loadCubiertaData(); + } + + + this.#initInterior(); + this.#initSeleccionTirada(); + this.#initExtras(); $(document).on('change', 'input[min][max]', function () { const $input = $(this); @@ -108,11 +157,10 @@ class PresupuestoCliente { } }); - const stored = sessionStorage.getItem("formData"); - if (stored) { - this.formData = JSON.parse(stored); - this.#loadDatosGeneralesData(); - } + $('.custom-nav .nav-link').on('click', function (e) { + e.preventDefault(); + e.stopImmediatePropagation(); // frena el handler de Bootstrap + }); } #cacheFormData() { @@ -122,22 +170,45 @@ class PresupuestoCliente { #changeTab(idContenidoTab) { const tabButton = document.querySelector(`[data-bs-target="#${idContenidoTab}"]`); if (tabButton) { - const tab = new bootstrap.Tab(tabButton); - tab.show(); + bootstrap.Tab.getOrCreateInstance(tabButton).show(); } } #getPresupuestoData() { - return { + let data = { ...this.#getDatosGeneralesData(), ...this.#getInteriorData(), - ...this.#getCubiertaData() + ...this.#getCubiertaData(), + selectedTirada: this.formData.selectedTirada }; + + const sobrecubierta = data.sobrecubierta; + const faja = data.faja; + + delete data.sobrecubierta; + delete data.faja; + data = { + ...data, + sobrecubierta: sobrecubierta.activo, + papelSobrecubiertaId: sobrecubierta.papelSobrecubiertaId, + gramajeSobrecubierta: sobrecubierta.gramajeSobrecubierta, + tamanioSolapasSobrecubierta: sobrecubierta.tamanioSolapasSobrecubierta, + acabadoSobrecubierta: sobrecubierta.acabado, + faja: faja.activo, + papelFajaId: faja.papelFajaId, + gramajeFaja: faja.gramajeFaja, + altoFaja: faja.alto, + tamanioSolapasFaja: faja.tamanioSolapasFaja, + acabadoFaja: faja.acabado + } + + return data; + } - #addGramaje(contenedor, gramaje, name) { + #addGramaje(contenedor, id_base, gramaje, name) { - const id = `gramaje-${gramaje}`; + const id = `${id_base}-${gramaje}`; // Crear input const input = document.createElement('input'); @@ -178,6 +249,7 @@ class PresupuestoCliente { else { this.divPosicionPaginasColor.removeClass('d-none'); } + this.paginas = parseInt(this.paginasNegro.val()) + parseInt(this.paginasColor.val()); this.#updateTipoEncuadernacion(); }); @@ -241,6 +313,8 @@ class PresupuestoCliente { this.datos_generales_alert.addClass('d-none'); + $('.alto-faja-max').text(`max: ${this.formData.datosGenerales.alto} mm`); + this.#loadInteriorData(data); const interiorData = this.#getInteriorData(); @@ -541,6 +615,8 @@ class PresupuestoCliente { }); this.divAcabadoCubierta.empty(); + this.acabadoSobrecubierta.empty(); + this.fajaSobrecubierta.empty(); for (const opcion of data.opciones_acabados_cubierta) { const item = ` @@ -549,16 +625,20 @@ class PresupuestoCliente { `; this.divAcabadoCubierta.append(item); + this.acabadoSobrecubierta.append(new Option(opcion.name, opcion['sk-id'])); + this.fajaSobrecubierta.append(new Option(opcion.name, opcion['sk-id'])); }; $(`input[type="radio"][name="acabado-cubierta"][sk-id="${this.formData.cubierta.acabado}"]`).prop('checked', true); + this.acabadoSobrecubierta.val(this.formData.cubierta.sobrecubierta.acabado); + this.fajaSobrecubierta.val(this.formData.cubierta.faja.acabado); - this.#loadCubiertaData(data); + this.#loadCubiertaData(); this.#changeTab('pills-cover'); - const dataInterior = this.#getInteriorData(); - this.#updateInteriorData(dataInterior); + const dataCubierta = this.#getCubiertaData(); + this.#updateCubiertaData(dataCubierta); this.#cacheFormData(); } @@ -628,13 +708,13 @@ class PresupuestoCliente { for (let i = 0; i < gramajes.length; i++) { const gramaje = gramajes[i]; - this.#addGramaje(this.divGramajeInterior, gramaje, 'gramaje-interior'); + this.#addGramaje(this.divGramajeInterior, 'gramaje-interior', gramaje, 'gramaje-interior'); // Seleccionar el gramaje por defecto if (this.formData.interior.gramajeInterior === '' && i === 0) { - $(`#gramaje-${gramaje}`).prop('checked', true); + $(`#gramaje-interior-${gramaje}`).prop('checked', true); } else if (this.formData.interior.gramajeInterior == gramaje) { - $(`#gramaje-${gramaje}`).prop('checked', true); + $(`#gramaje-interior-${gramaje}`).prop('checked', true); } } if (this.divGramajeInterior.find('input[type="radio"]:checked').length === 0) { @@ -763,6 +843,15 @@ class PresupuestoCliente { }); }); + $(document).on('click', '.gramaje-cubierta', (e) => { + + const inputId = $(e.currentTarget).attr('for'); + const gramaje = parseInt($('#' + inputId).data('gramaje')); + this.formData.cubierta.gramajeCubierta = gramaje; + this.#cacheFormData(); + + }); + $(document).on('change', '.datos-cubierta', (e) => { const dataToStore = this.#getCubiertaData(); @@ -775,21 +864,63 @@ class PresupuestoCliente { this.formData.cubierta.acabado = parseInt($(e.currentTarget).attr('sk-id')) || 1; this.#cacheFormData(); }); - - $('.btn-change-tab-cubierta').on('click', () => { - this.#changeTab('pills-inside'); - /*const data = this.#getPresupuestoData(); + + $(document).on('click', '.custom-toggle', (e) => { + + const $target = $(e.currentTarget); + if ($target.hasClass('active')) { + if ($target.attr('id') === 'sobrecubierta') { + this.divSobrecubierta.removeClass('d-none'); + } + else if ($target.attr('id') === 'faja') { + this.divFaja.removeClass('d-none'); + } + } else { + if ($target.attr('id') === 'sobrecubierta') { + this.divSobrecubierta.addClass('d-none'); + } + else if ($target.attr('id') === 'faja') { + this.divFaja.addClass('d-none'); + } + } + const dataToStore = this.#getCubiertaData(); + this.#updateCubiertaData(dataToStore); + this.#cacheFormData(); + }); + + $('.btn-change-tab-cubierta').on('click', (e) => { + + const data = this.#getPresupuestoData(); + const id = e.currentTarget.id; $.ajax({ - url: '/presupuesto/public/validar/cubierta', - type: 'POST', - data: data, + url: '/presupuesto/public/validar/cubierta', + type: 'POST', + data: data, success: (data) => { - this.#changeTab('pills-inside'); + if (id === 'btn-prev-cubierta') { + this.#changeTab('pills-inside'); + } + else { + $('#btn-next-seleccion-tirada').removeClass('d-none'); + this.#loadSeleccionTiradaData(data); + this.#changeTab('pills-seleccion-tirada'); + } }, error: (xhr, status, error) => { - console.error("Error al validar los datos de cubierta: ", xhr.responseText); + if (id === 'btn-prev-cubierta') { + this.#changeTab('pills-inside'); + return; + } + this.divTiradas.empty(); + this.divTiradasError.empty(); + this.divTiradas.addClass('d-none'); + this.divTiradasError.removeClass('d-none'); + $('#btn-next-seleccion-tirada').addClass('d-none'); + this.divTiradasError.append(''); + this.#changeTab('pills-seleccion-tirada'); } - });*/ + }); }); } @@ -833,20 +964,7 @@ class PresupuestoCliente { this.divPapelCubierta.find('.image-container').first().data('sk-id') || 3; } - const gramajesCubierta = data.opciones_gramaje_cubierta; - for (let i = 0; i < gramajesCubierta.length; i++) { - const gramaje = gramajesCubierta[i]; - this.#addGramaje(this.divGramajeCubierta, gramaje, 'gramaje-cubierta'); - - if (this.formData.cubierta.gramajeCubierta === '' && i === 0) { - $(`#gramaje-${gramaje}`).prop('checked', true); - } else if (this.formData.cubierta.gramajeCubierta == gramaje) { - $(`#gramaje-${gramaje}`).prop('checked', true); - } - } - if (this.divGramajeCubierta.find('input[type="radio"]:checked').length === 0) { - this.divGramajeCubierta.find('input[type="radio"]').first().prop('checked', true); - } + this.#addGramajesCubierta(data.opciones_gramaje_cubierta); const dataToStore = this.#getCubiertaData(); this.#updateCubiertaData(dataToStore); @@ -870,9 +988,20 @@ class PresupuestoCliente { const guardasGramaje = parseInt($('#papel-guardas option:selected').data('gramaje')) || 170; const guardasImpresas = parseInt(this.guardasImpresas) || 0; const cabezada = this.cabezada.val() || 'WHI'; - const papelCubiertaId = $('#div-papel-cubierta .image-container.selected').data('sk-id') || 3; - const gramajeCubierta = $('input[name="gramaje-cubierta"]:checked').data('gramaje') || 240; + const papelCubiertaId = $('#div-papel-cubierta .image-container.selected').data('sk-id') || this.formData.cubierta.papelCubiertaId || 3; + const gramajeCubierta = $('input[name="gramaje-cubierta"]:checked').data('gramaje') || this.formData.cubierta.gramajeCubierta || 170; const acabado = parseInt($(`input[name="acabado-cubierta"]:checked`).attr('sk-id')) || 1; + const sobrecubierta = this.sobrecubierta.hasClass('active'); + const papelSobrecubiertaId = $('#papel-sobrecubierta option:selected').data('papel-id') || 2; + const gramajeSobrecubierta = parseInt($('#papel-sobrecubierta option:selected').data('gramaje')) || 170; + const solapasSobrecubierta = parseInt(this.solapasSobrecubierta.val()) || 0; + const acabadoSobrecubierta = this.acabadoSobrecubierta.val() || 0; + const faja = this.faja.hasClass('active'); + const papelFajaId = $('#papel-faja option:selected').data('papel-id') || 2; + const gramajeFaja = parseInt($('#papel-faja option:selected').data('gramaje')) || 170; + const fajaAlto = parseInt(this.altoFaja.val()) || 50; + const solapasFaja = parseInt(this.solapasFaja.val()) || 0; + const acabadoFaja = this.acabadoFaja.val() || 0; return { tipoCubierta: tipoCubierta, @@ -885,7 +1014,22 @@ class PresupuestoCliente { cabezada: cabezada, papelCubiertaId: papelCubiertaId, gramajeCubierta: gramajeCubierta, - acabadado: acabado + acabado: acabado, + sobrecubierta: { + activo: sobrecubierta, + papelSobrecubiertaId: papelSobrecubiertaId, + gramajeSobrecubierta: gramajeSobrecubierta, + acabado: acabadoSobrecubierta, + tamanioSolapasSobrecubierta: solapasSobrecubierta + }, + faja: { + activo: faja, + papelFajaId: papelFajaId, + gramajeFaja: gramajeFaja, + alto: fajaAlto, + acabado: acabadoFaja, + tamanioSolapasFaja: solapasFaja + } }; } @@ -902,19 +1046,34 @@ class PresupuestoCliente { this.formData.cubierta.papelCubiertaId = data.papelCubiertaId; this.formData.cubierta.gramajeCubierta = data.gramajeCubierta; this.formData.cubierta.acabado = data.acabado; + this.formData.cubierta.sobrecubierta = { + activo: data.sobrecubierta.activo, + papelSobrecubiertaId: data.sobrecubierta.papelSobrecubiertaId, + gramajeSobrecubierta: data.sobrecubierta.gramajeSobrecubierta, + tamanioSolapasSobrecubierta: data.sobrecubierta.tamanioSolapasSobrecubierta, + acabado: data.sobrecubierta.acabado + }; + this.formData.cubierta.faja = { + activo: data.faja.activo, + papelFajaId: data.faja.papelFajaId, + gramajeFaja: data.faja.gramajeFaja, + tamanioSolapasFaja: data.faja.tamanioSolapasFaja, + acabado: data.faja.acabado, + alto: data.faja.alto + }; } #addGramajesCubierta(gramajes) { for (let i = 0; i < gramajes.length; i++) { const gramaje = gramajes[i]; - this.#addGramaje(this.divGramajeCubierta, gramaje, 'gramaje-cubierta'); + this.#addGramaje(this.divGramajeCubierta, 'gramaje-cubierta', gramaje, 'gramaje-cubierta datos-cubierta'); // Seleccionar el gramaje por defecto - if (this.formData.interior.gramajeCubierta === '' && i === 0) { - $(`#gramaje-${gramaje}`).prop('checked', true); - } else if (this.formData.interior.gramajeCubierta == gramaje) { - $(`#gramaje-${gramaje}`).prop('checked', true); + if (this.formData.cubierta.gramajeCubierta === '' && i === 0) { + $(`#gramaje-cubierta-${gramaje}`).prop('checked', true); + } else if (this.formData.cubierta.gramajeCubierta == gramaje) { + $(`#gramaje-cubierta-${gramaje}`).prop('checked', true); } } if (this.divGramajeCubierta.find('input[type="radio"]:checked').length === 0) { @@ -940,7 +1099,6 @@ class PresupuestoCliente { this.formData.cubierta.guardasGramaje + '"]').prop('selected', true).trigger('change'); this.guardasImpresas.val(this.formData.cubierta.guardasImpresas); this.cabezada.val(this.formData.cubierta.cabezada); - $(`input[type="radio"][name="acabado-cubierta"][sk-id="${this.formData.cubierta.acabado}"]`).prop('checked', true); } $(`#${this.formData.cubierta.tipoCubierta}`).trigger('click'); @@ -958,13 +1116,153 @@ class PresupuestoCliente { } this.carasImpresionCubierta.val(this.formData.cubierta.cubiertaCaras); + $(`input[type="radio"][name="acabado-cubierta"][sk-id="${this.formData.cubierta.acabado}"]`).prop('checked', true); + if (this.formData.cubierta.sobrecubierta.activo) { + this.sobrecubierta.addClass('active'); + this.divSobrecubierta.removeClass('d-none'); + } + else { + this.sobrecubierta.removeClass('active'); + this.divSobrecubierta.addClass('d-none'); + } + $('#papel-sobrecubierta option[data-papel-id="' + + this.formData.cubierta.sobrecubierta.papelSobrecubiertaId + '"][data-gramaje="' + + this.formData.cubierta.sobrecubierta.gramajeSobrecubierta + '"]').prop('selected', true); + this.solapasSobrecubierta.val(this.formData.cubierta.sobrecubierta.tamanioSolapasSobrecubierta); + this.acabadoSobrecubierta.val(this.formData.cubierta.sobrecubierta.acabado); - + if (this.formData.cubierta.faja.activo) { + this.faja.addClass('active'); + this.divFaja.removeClass('d-none'); + } + else { + this.faja.removeClass('active'); + this.divFaja.addClass('d-none'); + } + $('#papel-faja option[data-papel-id="' + + this.formData.cubierta.faja.papelFajaId + '"][data-gramaje="' + + this.formData.cubierta.faja.gramajeFaja + '"]').prop('selected', true); + this.solapasFaja.val(this.formData.cubierta.faja.tamanioSolapasFaja); + this.altoFaja.val(this.formData.cubierta.faja.alto); + this.acabadoFaja.val(this.formData.cubierta.faja.acabado); } /****************************** * END CUBIERTA ******************************/ + + + /****************************** + * SELECCION TIRADA + ******************************/ + #initSeleccionTirada() { + + $(document).on('click', '.btn-change-tab-seleccion-tirada', (e) => { + + const id = e.currentTarget.id; + + if (id === 'btn-prev-seleccion-tirada') { + this.#changeTab('pills-cover'); + } else { + const data = this.#getPresupuestoData(); + const id = e.currentTarget.id; + $.ajax({ + url: '/presupuesto/public/validar/seleccion-tirada', + type: 'POST', + data: data, + success: (data) => { + this.#loadExtrasData(data.servicios_extra); + this.#changeTab('pills-extras'); + }, + error: (xhr, status, error) => { + console.error("Error al validar los datos de selección de tirada: ", xhr.responseText); + } + }); + + } + }); + + this.divTiradas.off('tirada:changed.cache') // por si re-bindeas + .on('tirada:changed.cache', (e, detail) => { + // detail = { id, name, unidades, precioUnidad, precioTotal } + this.formData.selectedTirada = detail.unidades; + this.#cacheFormData(); + }); + } + + #loadSeleccionTiradaData(data) { + + this.divTiradasError.addClass('d-none'); + this.divTiradas.empty(); + this.divTiradas.removeClass('d-none'); + this.divTiradas.removeClass('animate-fadeInUpBounce'); + + const labels = { + perUnit: this.divTiradas.data('perUnit'), + total: this.divTiradas.data('total'), + select: this.divTiradas.data('select'), + selected: this.divTiradas.data('selected'), + units: this.divTiradas.data('units') + }; + + this.divTiradas.addClass('animate-fadeInUpBounce'); + for (let i = 0; i < data.tiradas.length; i++) { + const item = new TiradaCard({ + id: "tirada-" + data.tiradas[i], + unidades: data.tiradas[i], + precioUnidad: data.precios[i], + precioTotal: data.precios[i] * data.tiradas[i], + moneda: "€", + labels: labels, + selected: this.formData.selectedTirada == data.tiradas[i] + }); + this.divTiradas.append(item.renderCol(this.divTiradas)); + } + + if (this.divTiradas.find('.tirada-card input[type="radio"]:checked').length === 0) { + this.divTiradas.find('.tirada-card input[type="radio"]').first().prop('checked', true).trigger('change'); + this.formData.selectedTirada = this.divTiradas.find('.tirada-card input[type="radio"]').first().data('unidades') || data.tiradas[0]; + } + } + /****************************** + * END SELECCION TIRADA + ******************************/ + + + + /****************************** + * EXTRAS + ******************************/ + #initExtras() { + + $(document).on('click', '.btn-change-tab-extras', (e) => { + + const id = e.currentTarget.id; + + if (id === 'btn-prev-extras') { + this.#changeTab('pills-seleccion-tirada'); + } else { + //this.#changeTab('pills-finalizar'); + } + }); + } + + #loadExtrasData(servicios) { + + this.divExtras.empty(); + this.divExtras.removeClass('animate-fadeInUpBounce'); + + this.divExtras.addClass('animate-fadeInUpBounce'); + for (const extra of servicios) { + const item = new ServiceOptionCard(extra); + this.divExtras.append(item.render()); + } + } + + + /****************************** + * END EXTRAS + ******************************/ } diff --git a/src/main/resources/static/assets/js/pages/imprimelibros/presupuestador/service-option-card.js b/src/main/resources/static/assets/js/pages/imprimelibros/presupuestador/service-option-card.js new file mode 100644 index 0000000..e787859 --- /dev/null +++ b/src/main/resources/static/assets/js/pages/imprimelibros/presupuestador/service-option-card.js @@ -0,0 +1,46 @@ +class ServiceOptionCard { + + constructor({ id, title, description = '', price = '', priceUnit = '', checked = false, allowChange = true, ribbonText }) { + this.id = id; + this.title = title; + this.description = description ? description : ' '; + this.price = price; + this.priceUnit = priceUnit; + this.checked = checked; + this.allowChange = !(String(allowChange).toLowerCase() === "false"); + this.ribbonText = ribbonText; + } + + + render() { + const ribbonHtml = this.ribbonText + ? `
${this.ribbonText}
` + : ''; + const $card = $(` +
+ + +
+ `); + + const $checkbox = $card.find('input[type="checkbox"]'); + + if (!this.allowChange) { + // Deshabilita el cambio de estado visual + $checkbox.on('click', (e) => { + e.preventDefault(); + e.stopImmediatePropagation(); + return false; + }); + } + + return $card; + } +} + +export default ServiceOptionCard; \ No newline at end of file diff --git a/src/main/resources/static/assets/js/pages/imprimelibros/presupuestador/tirada-price-card.js b/src/main/resources/static/assets/js/pages/imprimelibros/presupuestador/tirada-price-card.js new file mode 100644 index 0000000..67d16b1 --- /dev/null +++ b/src/main/resources/static/assets/js/pages/imprimelibros/presupuestador/tirada-price-card.js @@ -0,0 +1,123 @@ +// ===== Clase ===== +class TiradaCard { + constructor({ id, titulo, unidades, precioUnidad, precioTotal, selected = false, moneda = '€', name = 'tirada', + labels = {}, locale = (document.documentElement.lang || navigator.language || 'es-ES') + }) { + this.id = id; + this.titulo = titulo; + this.unidades = unidades; + this.precioUnidad = precioUnidad; + this.precioTotal = precioTotal; + this.selected = selected; + this.moneda = moneda; + this.name = name; + this.locale = locale; + this.labels = Object.assign({ + perUnit: 'Precio por unidad', + total: 'Total', + select: 'Seleccionar tirada', + selected: 'Seleccionada', + units: 'UNIDADES' + }, labels); + this.$container = null; // se establece al renderizar + } + + #formatMoneyES(value, decimals = 2) { + return new Intl.NumberFormat(this.locale, { + minimumFractionDigits: decimals, + maximumFractionDigits: decimals + }).format(Number(value)); + } + + #title() { + if (this.titulo) return this.titulo; + if (this.unidades != null) return `${this.unidades} ${this.labels.units}`; + return ''; + } + + // pásale el contenedor donde van todas las tarjetas de este grupo + renderCol($container) { + this.$container = $container ? $($container) : $(document.body); + + const col = $(` +
+ +
+ `); + + // --- Delegación de eventos en el contenedor (una sola vez por grupo) --- + const boundKey = `tirada-bound-${this.name}`; + const groupName = this.name; + const $group = this.$container; + const labels = this.labels; + + if (!$group.data(boundKey)) { + $group.on('change', `input[type="radio"][name="${groupName}"]`, function () { + // radios del grupo + const $groupRadios = $group.find(`input[type="radio"][name="${groupName}"]`); + + // resetear todas las tarjetas del grupo + $groupRadios + .closest('.tirada-card') + .removeClass('selected') + .find('.btn-select-tirada') + .html(labels.select || 'Seleccionar tirada'); + + // marcar la tarjeta seleccionada y su botón + const $card = $(this).closest('.tirada-card'); + $card.find('.btn-select-tirada') + .html(labels.selected + ' '); + $card.addClass('selected'); + + const $input = $(this); + const detail = { + id: $input.attr('id'), + name: $input.attr('name'), + unidades: Number($input.data('unidades') ?? $input.val()), + precioUnidad: Number($input.data('precioUnidad')), + precioTotal: Number($input.data('precioTotal')) + }; + // jQuery: pasa 'detail' como segundo argumento del handler + $group.trigger('tirada:changed', [detail]); + + }); + + $group.data(boundKey, true); + } + + const $btnLocal = col.find('.btn-select-tirada'); + if (this.selected) { + $btnLocal.html(labels.selected + ' '); + } + + // --- Comportamiento de la tarjeta/botón (local a esta tarjeta) --- + const card = col.find('.tirada-card'); + + col.find('.btn-select-tirada').on('click', (e) => { + e.preventDefault(); + card.find('input[type="radio"]').prop('checked', true).trigger('change'); + }); + + card.on('click', function (e) { + if ($(e.target).is('.btn-select-tirada')) return; + $(this).find('input[type="radio"]').prop('checked', true).trigger('change'); + }); + + return col; + } +} + +export default TiradaCard; diff --git a/src/main/resources/templates/imprimelibros/presupuestos/presupuestador-items/_cubierta.html b/src/main/resources/templates/imprimelibros/presupuestos/presupuestador-items/_cubierta.html index e67a341..7110feb 100644 --- a/src/main/resources/templates/imprimelibros/presupuestos/presupuestador-items/_cubierta.html +++ b/src/main/resources/templates/imprimelibros/presupuestos/presupuestador-items/_cubierta.html @@ -46,7 +46,7 @@ - +
@@ -93,7 +93,8 @@
-

min: 60 mm

@@ -230,22 +231,174 @@

- +
- +
+ +
+
+
Extras +
+
+ Opciones adicionales para la cubierta
+
+ +
+ +
+
+
+ + + +
+
+ + +
+ +
+ + +
+

min: 60 mm

+

max: 120 mm

+
+
+ +
+ + +
+
+ +
+
+
+ +
+
+
+ + + +
+
+ + +
+ +
+ + +
+

min: 50 mm

+

max: 120 mm

+
+
+ +
+ + +
+

min: 60 mm

+

max: 120 mm

+
+
+ +
+ + +
+
+ +
+
+
+
+
+ + +
- -
diff --git a/src/main/resources/templates/imprimelibros/presupuestos/presupuestador-items/_datos-generales.html b/src/main/resources/templates/imprimelibros/presupuestos/presupuestador-items/_datos-generales.html index 8096d7c..13e5020 100644 --- a/src/main/resources/templates/imprimelibros/presupuestos/presupuestador-items/_datos-generales.html +++ b/src/main/resources/templates/imprimelibros/presupuestos/presupuestador-items/_datos-generales.html @@ -156,7 +156,7 @@
-
+
diff --git a/src/main/resources/templates/imprimelibros/presupuestos/presupuestador-items/_extras.html b/src/main/resources/templates/imprimelibros/presupuestos/presupuestador-items/_extras.html new file mode 100644 index 0000000..58775f9 --- /dev/null +++ b/src/main/resources/templates/imprimelibros/presupuestos/presupuestador-items/_extras.html @@ -0,0 +1,31 @@ +
+ + +
+
+
Extras +
+
Seleccione los extras + que desea añadir a su presupuesto.
+
+ +
+
+
+
+
+ + +
+ + +
+
\ No newline at end of file diff --git a/src/main/resources/templates/imprimelibros/presupuestos/presupuestador-items/_seleccion-tirada.html b/src/main/resources/templates/imprimelibros/presupuestos/presupuestador-items/_seleccion-tirada.html new file mode 100644 index 0000000..4461bd7 --- /dev/null +++ b/src/main/resources/templates/imprimelibros/presupuestos/presupuestador-items/_seleccion-tirada.html @@ -0,0 +1,41 @@ +
+ + +
+
+
Seleccion de + tirada +
+
Seleccione la tirada + deseada
+
+ +
+
+ +
+
+ +
+
+
+ + +
+ + +
+
\ No newline at end of file diff --git a/src/main/resources/templates/imprimelibros/presupuestos/presupuestador-items/_summary.html b/src/main/resources/templates/imprimelibros/presupuestos/presupuestador-items/_summary.html new file mode 100644 index 0000000..1f517bb --- /dev/null +++ b/src/main/resources/templates/imprimelibros/presupuestos/presupuestador-items/_summary.html @@ -0,0 +1,74 @@ +
+
+
+
+
+
Resumen presupuesto
+
+
+
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Libro
+ + Fresado
+ + 148 x 210 mm
+ + 148 x 210 mm
+ + 148 x 210 mm
+ + 148 x 210 mm
+ + + + + + + + + + + + +
Cubierta del libro
+

+
148 x 210 mm
+ +
+
+ +
+ +
+ \ No newline at end of file diff --git a/src/main/resources/templates/imprimelibros/presupuestos/presupuestador.html b/src/main/resources/templates/imprimelibros/presupuestos/presupuestador.html index 554bc2b..186abdb 100644 --- a/src/main/resources/templates/imprimelibros/presupuestos/presupuestador.html +++ b/src/main/resources/templates/imprimelibros/presupuestos/presupuestador.html @@ -36,9 +36,18 @@ +
- -
-
-
-
-
-
Order Summary
-
-
-
-
-
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
ProductProduct InfoPrice
-
- -
-
-
Sweatshirt for Men (Pink)
-

$ 119.99 x 2

-
$ 239.98
-
- -
-
-
Noise Evolve Smartwatch
-

$ 94.99 x 1

-
$ 94.99
-
- -
-
-
350 ml Glass Grocery Container
-

$ 24.99 x 1

-
$ 24.99
Sub Total :$ 359.96
Discount (VELZON15) : - $ 50.00
Shipping Charge :$ 24.99
Estimated Tax (12%): $ 18.20
Total (USD) : - - $353.15 - -
+ +
-
-
- -
- -
-
\ No newline at end of file