diff --git a/src/main/java/com/imprimelibros/erp/cart/CartController.java b/src/main/java/com/imprimelibros/erp/cart/CartController.java index 4bb4f78..9692451 100644 --- a/src/main/java/com/imprimelibros/erp/cart/CartController.java +++ b/src/main/java/com/imprimelibros/erp/cart/CartController.java @@ -4,14 +4,10 @@ import org.springframework.stereotype.Controller; import org.springframework.ui.Model; import org.springframework.web.bind.annotation.*; -import com.imprimelibros.erp.users.UserDetailsImpl; - import jakarta.servlet.http.HttpServletRequest; -import com.imprimelibros.erp.users.User; - import org.springframework.http.ResponseEntity; -import org.springframework.security.core.Authentication; +import com.imprimelibros.erp.common.Utils; import java.security.Principal; import java.util.Locale; @@ -27,32 +23,11 @@ public class CartController { this.service = service; } - /** - * Obtiene el ID de usuario desde tu seguridad. - * Adáptalo a tu UserDetails (e.g., SecurityContext con getId()) - */ - private Long currentUserId(Principal principal) { - if (principal == null) { - throw new IllegalStateException("Usuario no autenticado"); - } - - if (principal instanceof Authentication auth) { - Object principalObj = auth.getPrincipal(); - - if (principalObj instanceof UserDetailsImpl udi) { - return udi.getId(); - } else if (principalObj instanceof User u && u.getId() != null) { - return u.getId(); - } - } - - throw new IllegalStateException("No se pudo obtener el ID del usuario actual"); - } /** Vista del carrito */ @GetMapping public String viewCart(Model model, Principal principal, Locale locale) { - var items = service.listItems(currentUserId(principal), locale); + var items = service.listItems(Utils.currentUserId(principal), locale); model.addAttribute("items", items); return "imprimelibros/cart/cart"; // crea esta vista si quieres (tabla simple) } @@ -60,14 +35,14 @@ public class CartController { /** Añadir presupuesto via POST form */ @PostMapping("/add") public String add(@PathVariable(name = "presupuestoId", required = true) Long presupuestoId, Principal principal) { - service.addPresupuesto(currentUserId(principal), presupuestoId); + service.addPresupuesto(Utils.currentUserId(principal), presupuestoId); return "redirect:/cart"; } /** Añadir presupuesto con ruta REST (opcional) */ @PostMapping("/add/{presupuestoId}") public Object addPath(@PathVariable Long presupuestoId, Principal principal, HttpServletRequest request) { - service.addPresupuesto(currentUserId(principal), presupuestoId); + service.addPresupuesto(Utils.currentUserId(principal), presupuestoId); boolean isAjax = "XMLHttpRequest".equals(request.getHeader("X-Requested-With")); if (isAjax) { // Responder 200 con la URL a la que quieres ir @@ -83,13 +58,13 @@ public class CartController { public long getCount(Principal principal) { if (principal == null) return 0; - return service.countItems(currentUserId(principal)); + return service.countItems(Utils.currentUserId(principal)); } /** Eliminar línea por ID de item */ @DeleteMapping("/{itemId}/remove") public String remove(@PathVariable Long itemId, Principal principal) { - service.removeItem(currentUserId(principal), itemId); + service.removeItem(Utils.currentUserId(principal), itemId); return "redirect:/cart"; } @@ -97,14 +72,14 @@ public class CartController { @DeleteMapping("/delete/item/{presupuestoId}") @ResponseBody public String removeByPresupuesto(@PathVariable Long presupuestoId, Principal principal) { - service.removeByPresupuesto(currentUserId(principal), presupuestoId); + service.removeByPresupuesto(Utils.currentUserId(principal), presupuestoId); return "redirect:/cart"; } /** Vaciar carrito completo */ @DeleteMapping("/clear") public String clear(Principal principal) { - service.clear(currentUserId(principal)); + service.clear(Utils.currentUserId(principal)); return "redirect:/cart"; } } diff --git a/src/main/java/com/imprimelibros/erp/cart/CartService.java b/src/main/java/com/imprimelibros/erp/cart/CartService.java index d04b349..b529819 100644 --- a/src/main/java/com/imprimelibros/erp/cart/CartService.java +++ b/src/main/java/com/imprimelibros/erp/cart/CartService.java @@ -134,8 +134,15 @@ public class CartService { resumen.put("presupuestoId", presupuesto.getId()); + if(presupuesto.getServiciosJson() != null && presupuesto.getServiciosJson().contains("ejemplar-prueba")) { + resumen.put("hasSample", true); + } else { + resumen.put("hasSample", false); + } Map detalles = utils.getTextoPresupuesto(presupuesto, locale); + resumen.put("tirada", presupuesto.getSelectedTirada()); + resumen.put("baseTotal", Utils.formatCurrency(presupuesto.getBaseImponible(), locale)); resumen.put("base", presupuesto.getBaseImponible()); resumen.put("iva4", presupuesto.getIvaImporte4()); diff --git a/src/main/java/com/imprimelibros/erp/checkout/CheckoutController.java b/src/main/java/com/imprimelibros/erp/checkout/CheckoutController.java new file mode 100644 index 0000000..db83787 --- /dev/null +++ b/src/main/java/com/imprimelibros/erp/checkout/CheckoutController.java @@ -0,0 +1,86 @@ +package com.imprimelibros.erp.checkout; + +import java.security.Principal; +import java.util.List; +import java.util.Locale; +import java.util.Map; + +import org.springframework.context.MessageSource; +import org.springframework.http.HttpStatus; +import org.springframework.security.core.Authentication; +import org.springframework.stereotype.Controller; +import org.springframework.ui.Model; +import org.springframework.validation.BindingResult; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.server.ResponseStatusException; + +import com.imprimelibros.erp.common.Utils; +import com.imprimelibros.erp.direcciones.Direccion; +import com.imprimelibros.erp.i18n.TranslationService; +import com.imprimelibros.erp.paises.PaisesService; + +import jakarta.mail.Message; + +import com.imprimelibros.erp.direcciones.DireccionService; + +import com.imprimelibros.erp.cart.CartService; + +@Controller +@RequestMapping("/checkout") +public class CheckoutController { + + protected CartService cartService; + protected TranslationService translationService; + protected PaisesService paisesService; + protected DireccionService direccionService; + protected MessageSource messageSource; + + public CheckoutController(CartService cartService, TranslationService translationService, + PaisesService paisesService, DireccionService direccionService, MessageSource messageSource) { + this.cartService = cartService; + this.translationService = translationService; + this.paisesService = paisesService; + this.direccionService = direccionService; + this.messageSource = messageSource; + } + + @GetMapping + public String view(Model model, Principal principal, Locale locale) { + + List keys = List.of( + "app.cancelar", + "app.seleccionar", + "checkout.shipping.add.title", + "checkout.shipping.select-placeholder", + "checkout.shipping.new-address", + "app.yes", + "app.cancelar"); + + Map translations = translationService.getTranslations(locale, keys); + model.addAttribute("languageBundle", translations); + + var items = this.cartService.listItems(Utils.currentUserId(principal), locale); + for (var item : items) { + if (item.get("hasSample") != null && (Boolean) item.get("hasSample")) { + model.addAttribute("hasSample", true); + break; + } + } + model.addAttribute("items", items); + return "imprimelibros/checkout/checkout"; // crea esta vista si quieres (tabla simple) + } + + @GetMapping("/get-address/{id}") + public String getDireccionCard(@PathVariable Long id, Model model, Locale locale) { + Direccion dir = direccionService.findById(id) + .orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND)); + model.addAttribute("pais", messageSource.getMessage("paises." + dir.getPais().getKeyword(), null, + dir.getPais().getKeyword(), locale)); + model.addAttribute("direccion", dir); + + return "imprimelibros/direcciones/direccionCard :: direccionCard(direccion=${direccion})"; + } +} diff --git a/src/main/java/com/imprimelibros/erp/common/Utils.java b/src/main/java/com/imprimelibros/erp/common/Utils.java index b6b2a79..9d41573 100644 --- a/src/main/java/com/imprimelibros/erp/common/Utils.java +++ b/src/main/java/com/imprimelibros/erp/common/Utils.java @@ -2,6 +2,7 @@ package com.imprimelibros.erp.common; import java.math.BigDecimal; import java.math.RoundingMode; +import java.security.Principal; import java.text.NumberFormat; import java.util.ArrayList; import java.util.HashMap; @@ -12,6 +13,7 @@ import java.util.Optional; import java.util.function.BiFunction; import org.springframework.context.MessageSource; +import org.springframework.security.core.Authentication; import org.springframework.stereotype.Component; import com.fasterxml.jackson.core.JsonProcessingException; @@ -22,6 +24,8 @@ import com.imprimelibros.erp.presupuesto.classes.PresupuestoFormatter; import com.imprimelibros.erp.presupuesto.dto.Presupuesto; import com.imprimelibros.erp.presupuesto.maquetacion.MaquetacionMatrices; import com.imprimelibros.erp.presupuesto.marcapaginas.Marcapaginas; +import com.imprimelibros.erp.users.User; +import com.imprimelibros.erp.users.UserDetailsImpl; import jakarta.persistence.criteria.CriteriaBuilder; import jakarta.persistence.criteria.Path; @@ -40,6 +44,24 @@ public class Utils { this.messageSource = messageSource; } + public static Long currentUserId(Principal principal) { + + if (principal == null) { + throw new IllegalStateException("Usuario no autenticado"); + } + + if (principal instanceof Authentication auth) { + Object principalObj = auth.getPrincipal(); + + if (principalObj instanceof UserDetailsImpl udi) { + return udi.getId(); + } else if (principalObj instanceof User u && u.getId() != null) { + return u.getId(); + } + } + throw new IllegalStateException("No se pudo obtener el ID del usuario actual"); + } + public static String formatCurrency(BigDecimal amount, Locale locale) { NumberFormat currencyFormatter = NumberFormat.getCurrencyInstance(locale); return currencyFormatter.format(amount); diff --git a/src/main/java/com/imprimelibros/erp/direcciones/DireccionController.java b/src/main/java/com/imprimelibros/erp/direcciones/DireccionController.java index 8993f40..cf05771 100644 --- a/src/main/java/com/imprimelibros/erp/direcciones/DireccionController.java +++ b/src/main/java/com/imprimelibros/erp/direcciones/DireccionController.java @@ -1,5 +1,6 @@ package com.imprimelibros.erp.direcciones; +import java.security.Principal; import java.time.Instant; import java.util.ArrayList; import java.util.List; @@ -43,20 +44,23 @@ import jakarta.validation.Valid; @RequestMapping("/direcciones") public class DireccionController { + private final DireccionService direccionService; + protected final DireccionRepository repo; protected final PaisesService paisesService; protected final MessageSource messageSource; protected final UserDao userRepo; protected final TranslationService translationService; - public DireccionController(DireccionRepository repo, PaisesService paisesService, - MessageSource messageSource, UserDao userRepo, TranslationService translationService) { + MessageSource messageSource, UserDao userRepo, TranslationService translationService, + DireccionService direccionService) { this.repo = repo; this.paisesService = paisesService; this.messageSource = messageSource; this.userRepo = userRepo; this.translationService = translationService; + this.direccionService = direccionService; } @GetMapping() @@ -295,6 +299,33 @@ public class DireccionController { return "imprimelibros/direcciones/direccion-form :: direccionForm"; } + @GetMapping("direction-form") + public String getForm(@RequestParam(required = false) Long id, + Direccion direccion, + BindingResult binding, + Model model, + HttpServletResponse response, + Principal principal, + Locale locale) { + + model.addAttribute("paises", paisesService.getForSelect("", "", locale).get("results")); + + Direccion newDireccion = new Direccion(); + + User user = null; + if (principal instanceof UserDetailsImpl udi) { + user = new User(); + user.setId(udi.getId()); + } else if (principal instanceof User u && u.getId() != null) { + user = u; + } + newDireccion.setUser(user); + model.addAttribute("dirForm", newDireccion); + model.addAttribute("action", "/direcciones/add"); + + return "imprimelibros/direcciones/direccion-form-fixed-user :: direccionForm"; + } + @PostMapping public String create( @Valid @ModelAttribute("dirForm") Direccion direccion, @@ -327,6 +358,34 @@ public class DireccionController { return null; } + // para el formulario modal en checkout + @PostMapping("/add") + public String create2( + @Valid @ModelAttribute("dirForm") Direccion direccion, + BindingResult binding, + Model model, + HttpServletResponse response, + Authentication auth, + Locale locale) { + + User current = userRepo.findByUserNameIgnoreCaseAndEnabledTrueAndDeletedFalse(auth.getName()).orElse(null); + direccion.setUser(current); + + if (binding.hasErrors()) { + response.setStatus(422); + model.addAttribute("paises", paisesService.getForSelect("", "", locale).get("results")); + model.addAttribute("action", "/direcciones/add"); + model.addAttribute("dirForm", direccion); + return "imprimelibros/direcciones/direccion-form-fixed-user :: direccionForm"; + } + + var data = direccion; + + repo.save(data); + response.setStatus(201); + return null; + } + @PostMapping("/{id}") public String update( @PathVariable Long id, @@ -416,12 +475,33 @@ public class DireccionController { } } + @GetMapping(value = "/select2", produces = "application/json") + @ResponseBody + public Map getSelect2( + @RequestParam(value = "q", required = false) String q1, + @RequestParam(value = "term", required = false) String q2, + Authentication auth) { + + boolean isAdmin = auth.getAuthorities().stream() + .anyMatch(a -> a.getAuthority().equals("ROLE_ADMIN") || a.getAuthority().equals("ROLE_SUPERADMIN")); + + Long currentUserId = null; + if (auth != null && auth.getPrincipal() instanceof UserDetailsImpl udi) { + currentUserId = udi.getId(); + } else if (auth != null) { + currentUserId = userRepo.findIdByUserNameIgnoreCase(auth.getName()).orElse(null); + } + + return direccionService.getForSelect(q1, q2, isAdmin ? null : currentUserId); + + } + private boolean isOwnerOrAdmin(Authentication auth, Long ownerId) { if (auth == null) { return false; } boolean isAdmin = auth.getAuthorities().stream() - .anyMatch(a -> a.getAuthority().equals("ROLE_ADMIN")); + .anyMatch(a -> a.getAuthority().equals("ROLE_ADMIN") || a.getAuthority().equals("ROLE_SUPERADMIN")); if (isAdmin) { return true; } @@ -434,4 +514,5 @@ public class DireccionController { } return currentUserId != null && currentUserId.equals(ownerId); } + } diff --git a/src/main/java/com/imprimelibros/erp/direcciones/DireccionService.java b/src/main/java/com/imprimelibros/erp/direcciones/DireccionService.java new file mode 100644 index 0000000..116d3e0 --- /dev/null +++ b/src/main/java/com/imprimelibros/erp/direcciones/DireccionService.java @@ -0,0 +1,86 @@ +package com.imprimelibros.erp.direcciones; + +import java.text.Collator; +import java.util.Comparator; +import java.util.HashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Optional; +import java.util.stream.Collectors; + +import org.springframework.stereotype.Service; +import com.imprimelibros.erp.direcciones.DireccionRepository; +import com.imprimelibros.erp.paises.Paises; + +@Service +public class DireccionService { + + protected DireccionRepository repo; + + public DireccionService(DireccionRepository repo) { + this.repo = repo; + } + + public Map getForSelect(String q1, String q2, Long userId) { + try { + + // Termino de búsqueda (Select2 usa 'q' o 'term' según versión/config) + String search = Optional.ofNullable(q1).orElse(q2); + if (search != null) { + search = search.trim(); + } + final String q = (search == null || search.isEmpty()) + ? null + : search.toLowerCase(); + + List all = userId != null ? repo.findByUserId(userId) : repo.findAll(); + + // Mapear a opciones id/text con i18n y filtrar por búsqueda si llega + List> options = all.stream() + .map(cc -> { + String id = cc.getId().toString(); + String alias = cc.getAlias(); + String direccion = cc.getDireccion(); + String cp = String.valueOf(cc.getCp()); + String ciudad = cc.getCiudad(); + String att = cc.getAtt(); + Map m = new HashMap<>(); + m.put("id", id); // lo normal en Select2: id = valor que guardarás (code3) + m.put("text", alias); // texto mostrado, i18n con fallback a keyword + m.put("cp", cp); + m.put("ciudad", ciudad); + m.put("att", att); + m.put("alias", alias); + m.put("direccion", direccion); + return m; + }) + .filter(opt -> { + if (q == null || q.isEmpty()) + return true; + String cp = opt.get("cp"); + String ciudad = opt.get("ciudad").toLowerCase(); + String att = opt.get("att").toLowerCase(); + String alias = opt.get("alias").toLowerCase(); + String text = opt.get("text").toLowerCase(); + String direccion = opt.get("direccion").toLowerCase(); + return text.contains(q) || cp.contains(q) || ciudad.contains(q) || att.contains(q) || alias.contains(q) || direccion.contains(q); + }) + .sorted(Comparator.comparing(m -> m.get("text"), Collator.getInstance())) + .collect(Collectors.toList()); + + // Estructura Select2 + Map resp = new HashMap<>(); + resp.put("results", options); + return resp; + } catch (Exception e) { + e.printStackTrace(); + return Map.of("results", List.of()); + } + } + + public Optional findById(Long id) { + return repo.findById(id); + } + +} diff --git a/src/main/java/com/imprimelibros/erp/externalApi/skApiClient.java b/src/main/java/com/imprimelibros/erp/externalApi/skApiClient.java index 4e8fcb3..26bbd76 100644 --- a/src/main/java/com/imprimelibros/erp/externalApi/skApiClient.java +++ b/src/main/java/com/imprimelibros/erp/externalApi/skApiClient.java @@ -6,6 +6,7 @@ import org.springframework.http.*; import org.springframework.stereotype.Service; import org.springframework.web.client.HttpClientErrorException; import org.springframework.web.client.RestTemplate; +import org.springframework.web.util.UriComponentsBuilder; import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.ObjectMapper; @@ -20,6 +21,7 @@ import com.imprimelibros.erp.presupuesto.dto.Presupuesto.TipoEncuadernacion; import java.util.Map; import java.math.BigDecimal; import java.math.RoundingMode; +import java.net.URI; import java.util.HashMap; import java.util.List; import java.util.function.Supplier; @@ -219,6 +221,49 @@ public class skApiClient { } } + public Map getCosteEnvio(Map data, Locale locale) { + + return performWithRetryMap(() -> { + String url = this.skApiUrl + "api/calcular-envio"; + + URI uri = UriComponentsBuilder.fromUriString(url) + .queryParam("pais_code3", data.get("pais_code3")) + .queryParam("cp", data.get("cp")) + .queryParam("peso", data.get("peso")) + .queryParam("unidades", data.get("unidades")) + .queryParam("palets", data.get("palets")) + .build(true) // no re-encode [] + .toUri(); + HttpHeaders headers = new HttpHeaders(); + headers.setBearerAuth(authService.getToken()); + + + ResponseEntity response = restTemplate.exchange( + uri, + HttpMethod.GET, + new HttpEntity<>(headers), + String.class); + + try { + Map responseBody = new ObjectMapper().readValue( + response.getBody(), + new TypeReference>() { + }); + Boolean error = (Boolean) responseBody.get("error"); + if (error != null && error) { + return Map.of("error", messageSource.getMessage("direcciones.error.noShippingCost", null, locale)); + } else { + Double total = (Double) responseBody.get("data"); + return Map.of("data", total); + } + } catch (JsonProcessingException e) { + e.printStackTrace(); + return Map.of("error", "Internal Server Error: 1"); // Fallback en caso de error + } + }); + + } + /****************** * PRIVATE METHODS ******************/ @@ -236,6 +281,20 @@ public class skApiClient { } } + private Map performWithRetryMap(Supplier> request) { + try { + return request.get(); + } catch (HttpClientErrorException.Unauthorized e) { + // Token expirado, renovar y reintentar + authService.invalidateToken(); + try { + return request.get(); // segundo intento + } catch (HttpClientErrorException ex) { + throw new RuntimeException("La autenticación ha fallado tras renovar el token.", ex); + } + } + } + private static BigDecimal calcularMargen( BigDecimal importe, BigDecimal importeMin, BigDecimal importeMax, BigDecimal margenMax, BigDecimal margenMin) { diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index d56bf95..2014254 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -11,8 +11,8 @@ logging.level.org.springframework=ERROR # # Database Configuration # -spring.datasource.url=jdbc:mysql://localhost:3309/imprimelibros -#spring.datasource.url=jdbc:mysql://127.0.0.1:3309/imprimelibros?useSSL=false&allowPublicKeyRetrieval=true&serverTimezone=Europe/Madrid&characterEncoding=utf8 +#spring.datasource.url=jdbc:mysql://localhost:3309/imprimelibros +spring.datasource.url=jdbc:mysql://127.0.0.1:3309/imprimelibros?useSSL=false&allowPublicKeyRetrieval=true&serverTimezone=Europe/Madrid&characterEncoding=utf8 spring.datasource.username=imprimelibros_user spring.datasource.password=om91irrDctd spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver @@ -24,8 +24,8 @@ spring.jpa.show-sql=false # # Safekat API Configuration # -safekat.api.url=http://localhost:8000/ -#safekat.api.url=https://erp-dev.safekat.es/ +#safekat.api.url=http://localhost:8000/ +safekat.api.url=https://erp-dev.safekat.es/ safekat.api.email=imnavajas@coit.es safekat.api.password=Safekat2024 diff --git a/src/main/resources/i18n/app_es.properties b/src/main/resources/i18n/app_es.properties index 8e2de87..4ce7940 100644 --- a/src/main/resources/i18n/app_es.properties +++ b/src/main/resources/i18n/app_es.properties @@ -3,6 +3,7 @@ app.yes=Sí app.no=No app.aceptar=Aceptar app.cancelar=Cancelar +app.seleccionar=Seleccionar app.guardar=Guardar app.editar=Editar app.add=Añadir diff --git a/src/main/resources/i18n/direcciones_es.properties b/src/main/resources/i18n/direcciones_es.properties index bc30637..6e3ee47 100644 --- a/src/main/resources/i18n/direcciones_es.properties +++ b/src/main/resources/i18n/direcciones_es.properties @@ -53,5 +53,7 @@ direcciones.error.delete-internal-error=Error interno al eliminar la dirección. direcciones.error.noEncontrado=Dirección no encontrada. direcciones.error.sinPermiso=No tiene permiso para realizar esta acción. +direcciones.error.noShippingCost=No se pudo calcular el coste de envío para la dirección proporcionada. + direcciones.form.error.required=Campo obligatorio. diff --git a/src/main/resources/i18n/pedidos_en.properties b/src/main/resources/i18n/pedidos_en.properties new file mode 100644 index 0000000..e69de29 diff --git a/src/main/resources/i18n/pedidos_es.properties b/src/main/resources/i18n/pedidos_es.properties new file mode 100644 index 0000000..c23bb54 --- /dev/null +++ b/src/main/resources/i18n/pedidos_es.properties @@ -0,0 +1,17 @@ +checkout.title=Finalizar compra +checkout.summay=Resumen de la compra +checkout.shipping=Envío +checkout.payment=Método de pago + +checkout.shipping.info=Todos los pedidos incluyen un envío gratuito a la Península y Baleares por línea de pedido. +checkout.shipping.order=Envío del pedido +checkout.shipping.samples=Envío de pruebas +checkout.shipping.onlyOneShipping=Todo el pedido se envía a una única dirección. +checkout.shipping.add=Añadir dirección +checkout.shipping.add.title=Seleccione una dirección +checkout.shipping.select-placeholder=Buscar en direcciones... +checkout.shipping.new-address=Nueva dirección + +checkout.summary.presupuesto=#Presupuesto +checkout.summary.titulo=Título +checkout.summary.base=Base \ No newline at end of file diff --git a/src/main/resources/static/assets/js/pages/imprimelibros/checkout/checkout.js b/src/main/resources/static/assets/js/pages/imprimelibros/checkout/checkout.js new file mode 100644 index 0000000..ea7c10b --- /dev/null +++ b/src/main/resources/static/assets/js/pages/imprimelibros/checkout/checkout.js @@ -0,0 +1,193 @@ +$(() => { + + const csrfToken = document.querySelector('meta[name="_csrf"]')?.getAttribute('content'); + const csrfHeader = document.querySelector('meta[name="_csrf_header"]')?.getAttribute('content'); + if (window.$ && csrfToken && csrfHeader) { + $.ajaxSetup({ + beforeSend: function (xhr) { + xhr.setRequestHeader(csrfHeader, csrfToken); + } + }); + } + + const language = document.documentElement.lang || 'es-ES'; + const modalEl = document.getElementById('direccionFormModal'); + const modal = bootstrap.Modal.getOrCreateInstance(modalEl); + + + $("#onlyOneShipping").on('change', function () { + if ($(this).is(':checked')) { + $('#shippingAddressesContainer').empty().show(); + $('#shippingMultipleAddressesContainer').show(); + } else { + $('#shippingAddressesContainer').hide(); + $('#shippingMultipleAddressesContainer').empty().hide(); + } + }); + + $('#addOrderAddress').on('click', () => { + if ($('#onlyOneShipping').is(':checked')) { + seleccionarDireccionEnvio(); + } else { + // Add address to multiple shipping addresses container + } + }); + + async function seleccionarDireccionEnvio() { + + const { value: direccionId, isDenied } = await Swal.fire({ + title: window.languageBundle['checkout.shipping.add.title'] || 'Seleccione una dirección', + html: ` + + `, + showCancelButton: true, + showDenyButton: true, + buttonsStyling: false, + confirmButtonText: window.languageBundle['app.seleccionar'] || 'Seleccionar', + cancelButtonText: window.languageBundle['app.cancelar'] || 'Cancelar', + denyButtonText: window.languageBundle['checkout.shipping.new-address'] || 'Nueva dirección', + customClass: { + confirmButton: 'btn btn-secondary me-2', + cancelButton: 'btn btn-light', + denyButton: 'btn btn-secondary me-2' + }, + focusConfirm: false, + + // Inicializa cuando el DOM del modal ya existe + didOpen: () => { + const $select = $('#direccionSelect'); + $select.empty(); // limpia placeholder estático + + $select.select2({ + width: '100%', + dropdownParent: $('.swal2-container'), + ajax: { + url: '/direcciones/select2', + dataType: 'json', + delay: 250, + data: params => ({ q: params.term || '' }), + processResults: (data) => { + const items = Array.isArray(data) ? data : (data.results || []); + return { + results: items.map(item => ({ + id: item.id, + text: item.text, // ← Select2 necesita 'id' y 'text' + alias: item.alias || 'Sin alias', + att: item.att || '', + direccion: item.direccion || '', + cp: item.cp || '', + ciudad: item.ciudad || '', + html: ` +
+ ${item.alias || 'Sin alias'}
+ ${item.att ? `${item.att}
` : ''} + ${item.direccion || ''}${item.cp ? ', ' + item.cp : ''}${item.ciudad ? ', ' + item.ciudad : ''} +
+ ` + })), + pagination: { more: false } // opcional, evita que espere más páginas + }; + } + + }, + placeholder: window.languageBundle['checkout.shipping.select-placeholder'] || 'Buscar en direcciones...', + language: language, + + templateResult: data => { + if (data.loading) return data.text; + return $(data.html || data.text); + }, + // Selección más compacta (solo alias + ciudad) + templateSelection: data => { + if (!data.id) return data.text; + const alias = data.alias || data.text; + const ciudad = data.ciudad ? ` — ${data.ciudad}` : ''; + return $(`${alias}${ciudad}`); + }, + escapeMarkup: m => m + }); + + // (Opcional) Prefijar valor si ya tienes una dirección elegida: + // const preselected = { id: '123', text: 'Oficina Central — Madrid' }; + // const option = new Option(preselected.text, preselected.id, true, true); + // $select.append(option).trigger('change'); + }, + + preConfirm: () => { + const $select = $('#direccionSelect'); + const val = $select.val(); + if (!val) { + Swal.showValidationMessage( + lang.startsWith('es') ? 'Debes seleccionar una dirección' : 'You must select an address' + ); + return false; + } + return val; + }, + + didClose: () => { + // Limpieza: destruir select2 para evitar fugas + const $select = $('#direccionSelect'); + if ($select.data('select2')) { + $select.select2('destroy'); + } + } + }); + + if (isDenied) { + $.get('/direcciones/direction-form', function (html) { + $('#direccionFormModalBody').html(html); + const title = $('#direccionFormModalBody #direccionForm').data('add'); + $('#direccionFormModal .modal-title').text(title); + modal.show(); + }); + } + + if (direccionId) { + // Obtén el objeto completo seleccionado + const response = await fetch(`/checkout/get-address/${direccionId}`); + if (response.ok) { + const html = await response.text(); + $('#shippingAddressesContainer').html(html); + } + } + } + + $(document).on('submit', '#direccionForm', function (e) { + e.preventDefault(); + const $form = $(this); + + $.ajax({ + url: $form.attr('action'), + type: 'POST', // PUT simulado via _method + data: $form.serialize(), + dataType: 'html', + success: function (html) { + // Si por cualquier motivo llega 200 con fragmento, lo insertamos igual + if (typeof html === 'string' && html.indexOf('id="direccionForm"') !== -1 && html.indexOf(' 0; + const title = $('#direccionFormModalBody #direccionForm').data(isEdit ? 'edit' : 'add'); + $('#direccionModal .modal-title').text(title); + return; + } + // Éxito real: cerrar y recargar tabla + modal.hide(); + seleccionarDireccionEnvio(); + }, + error: function (xhr) { + // Con 422 devolvemos el fragmento con errores aquí + if (xhr.status === 422 && xhr.responseText) { + $('#direccionFormModalBody').html(xhr.responseText); + const isEdit = $('#direccionFormModalBody #direccionForm input[name="_method"][value="PUT"]').length > 0; + const title = $('#direccionFormModalBody #direccionForm').data(isEdit ? 'edit' : 'add'); + $('#direccionModal .modal-title').text(title); + initSelect2Cliente(true); + return; + } + // Fallback + $('#direccionFormModalBody').html('
Error inesperado.
'); + } + }); + }); +}); diff --git a/src/main/resources/templates/imprimelibros/cart/cart.html b/src/main/resources/templates/imprimelibros/cart/cart.html index d54ce93..4cee29e 100644 --- a/src/main/resources/templates/imprimelibros/cart/cart.html +++ b/src/main/resources/templates/imprimelibros/cart/cart.html @@ -36,7 +36,28 @@
+ +
+
+
+

+
+ + +
+ + +
+ +
+
+
@@ -67,13 +88,14 @@ : - + @@ -83,7 +105,8 @@ diff --git a/src/main/resources/templates/imprimelibros/checkout/_envio.html b/src/main/resources/templates/imprimelibros/checkout/_envio.html new file mode 100644 index 0000000..15687fa --- /dev/null +++ b/src/main/resources/templates/imprimelibros/checkout/_envio.html @@ -0,0 +1,43 @@ +
+
+
+
+
+
Envio del pedido +
+
+
+
+

+
+ + +
+ + +
+
+ +
+
+
+ +
+
+
Envio de pruebas +
+
+ +
+ +
+
+ + +
\ No newline at end of file diff --git a/src/main/resources/templates/imprimelibros/checkout/_pago.html b/src/main/resources/templates/imprimelibros/checkout/_pago.html new file mode 100644 index 0000000..42eb8c5 --- /dev/null +++ b/src/main/resources/templates/imprimelibros/checkout/_pago.html @@ -0,0 +1,3 @@ +
+ +
\ No newline at end of file diff --git a/src/main/resources/templates/imprimelibros/checkout/checkout.html b/src/main/resources/templates/imprimelibros/checkout/checkout.html new file mode 100644 index 0000000..5763197 --- /dev/null +++ b/src/main/resources/templates/imprimelibros/checkout/checkout.html @@ -0,0 +1,175 @@ + + + + + + + + + + + + + + + + +
+
+ + +
+ +
+ +
+ +
+
+
+
+
+ + +
+
+
+ +
+
+
+ + +
+ +
+
+
+ +
+
+
+
+ +
+
+
+
+
+
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
PresupuestoTítulo + Base
+ PRESUPUESTO-001 + + Título del presupuesto + + + 0,00 + + +
:
:
: + + +
+ +
+ +
+
+ + +
+ + +
+
+ + + + + + + + + + + + \ No newline at end of file diff --git a/src/main/resources/templates/imprimelibros/direcciones/direccion-form-fixed-user.html b/src/main/resources/templates/imprimelibros/direcciones/direccion-form-fixed-user.html new file mode 100644 index 0000000..6980f89 --- /dev/null +++ b/src/main/resources/templates/imprimelibros/direcciones/direccion-form-fixed-user.html @@ -0,0 +1,169 @@ +
+
+ +
+ Error +
+ + + +
+ + +
+ +
+ +
+ + +
+
+ +
+ + +
+
+ +
+
+ + +
+
+ +
+ + +
+
+
+ +
+
+ + +
+
+ +
+ + +
+
+
+ +
+ + +
+
+ +
+ + +
+
+
+ +
+ + +
+ +
+ + +
+
+ +
+
+ + +
+
+ +
+ + +
+
+
+ +
+ +
+ +
+
\ No newline at end of file diff --git a/src/main/resources/templates/imprimelibros/direcciones/direccion-form.html b/src/main/resources/templates/imprimelibros/direcciones/direccion-form.html index d124b90..79e2969 100644 --- a/src/main/resources/templates/imprimelibros/direcciones/direccion-form.html +++ b/src/main/resources/templates/imprimelibros/direcciones/direccion-form.html @@ -103,6 +103,7 @@
diff --git a/src/main/resources/templates/imprimelibros/direcciones/direccionCard.html b/src/main/resources/templates/imprimelibros/direcciones/direccionCard.html new file mode 100644 index 0000000..157a45e --- /dev/null +++ b/src/main/resources/templates/imprimelibros/direcciones/direccionCard.html @@ -0,0 +1,18 @@ +
+
+
+
Alias
+ +

Att

+ +

Calle

+

CP Ciudad

+

País

+
+
+
diff --git a/src/main/resources/templates/imprimelibros/login/login.html b/src/main/resources/templates/imprimelibros/login/login.html index e3f3cda..1b2f168 100644 --- a/src/main/resources/templates/imprimelibros/login/login.html +++ b/src/main/resources/templates/imprimelibros/login/login.html @@ -6,6 +6,9 @@ + + + diff --git a/src/test/java/com/imprimelibros/erp/calcularEnvios.java b/src/test/java/com/imprimelibros/erp/calcularEnvios.java new file mode 100644 index 0000000..c3364b1 --- /dev/null +++ b/src/test/java/com/imprimelibros/erp/calcularEnvios.java @@ -0,0 +1,44 @@ +package com.imprimelibros.erp; + +import static org.junit.jupiter.api.Assertions.*; + +import java.util.HashMap; +import java.util.Map; + +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import java.util.Locale; + +import com.imprimelibros.erp.externalApi.skApiClient; + +@SpringBootTest +public class calcularEnvios { + @Autowired + private skApiClient apiClient; + + @Test + void testPrecioCalculadoDevuelveJson() { + + Map data = new HashMap<>(); + data.put("pais_code3", "esp"); + data.put("cp", 18200); + data.put("peso", 7.82); + data.put("unidades", 10); + data.put("palets", 0); + + + // get locale + Locale locale = Locale.forLanguageTag("es-ES"); + + + Map resultado = apiClient.getCosteEnvio(data, locale); + + System.out.println("📦 Resultado de la API:"); + System.out.println(resultado); + + assertNotNull(resultado, "El resultado no debe ser null"); + /*assertTrue(resultado.trim().startsWith("{"), "El resultado debe comenzar con { (JSON)"); + assertTrue(resultado.trim().endsWith("}"), "El resultado debe terminar con } (JSON)");*/ + } +}