diff --git a/src/main/java/com/imprimelibros/erp/cart/CartController.java b/src/main/java/com/imprimelibros/erp/cart/CartController.java index 9692451..6ffd926 100644 --- a/src/main/java/com/imprimelibros/erp/cart/CartController.java +++ b/src/main/java/com/imprimelibros/erp/cart/CartController.java @@ -3,13 +3,20 @@ package com.imprimelibros.erp.cart; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; import org.springframework.web.bind.annotation.*; +import org.springframework.web.server.ResponseStatusException; import jakarta.servlet.http.HttpServletRequest; +import org.springframework.context.MessageSource; +import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import com.imprimelibros.erp.common.Utils; +import com.imprimelibros.erp.direcciones.Direccion; +import com.imprimelibros.erp.direcciones.DireccionService; +import com.imprimelibros.erp.i18n.TranslationService; import java.security.Principal; +import java.util.List; import java.util.Locale; import java.util.Map; @@ -17,18 +24,38 @@ import java.util.Map; @RequestMapping("/cart") public class CartController { - private final CartService service; + protected final CartService service; + protected DireccionService direccionService; + protected MessageSource messageSource; + protected TranslationService translationService; - public CartController(CartService service) { + public CartController(CartService service, DireccionService direccionService, MessageSource messageSource, TranslationService translationService) { this.service = service; + this.direccionService = direccionService; + this.messageSource = messageSource; + this.translationService = translationService; } - /** Vista del carrito */ @GetMapping public String viewCart(Model model, Principal principal, Locale locale) { + + List keys = List.of( + "app.cancelar", + "app.seleccionar", + "cart.shipping.add.title", + "cart.shipping.select-placeholder", + "cart.shipping.new-address", + "cart.shipping.errors.noAddressSelected", + "app.yes", + "app.cancelar"); + + Map translations = translationService.getTranslations(locale, keys); + model.addAttribute("languageBundle", translations); + var items = service.listItems(Utils.currentUserId(principal), locale); model.addAttribute("items", items); + model.addAttribute("cartId", service.getOrCreateActiveCart(Utils.currentUserId(principal))); return "imprimelibros/cart/cart"; // crea esta vista si quieres (tabla simple) } @@ -82,4 +109,15 @@ public class CartController { service.clear(Utils.currentUserId(principal)); return "redirect:/cart"; } + + @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/checkout/CheckoutController.java b/src/main/java/com/imprimelibros/erp/checkout/CheckoutController.java index db83787..c8a3810 100644 --- a/src/main/java/com/imprimelibros/erp/checkout/CheckoutController.java +++ b/src/main/java/com/imprimelibros/erp/checkout/CheckoutController.java @@ -72,15 +72,4 @@ public class CheckoutController { 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/config/SecurityConfig.java b/src/main/java/com/imprimelibros/erp/config/SecurityConfig.java index 392b416..096488d 100644 --- a/src/main/java/com/imprimelibros/erp/config/SecurityConfig.java +++ b/src/main/java/com/imprimelibros/erp/config/SecurityConfig.java @@ -91,7 +91,6 @@ public class SecurityConfig { // Ignora CSRF para tu recurso público (sin Ant/Mvc matchers) .csrf(csrf -> csrf .ignoringRequestMatchers(pathStartsWith("/presupuesto/public/"))) - // ====== RequestCache: sólo navegaciones HTML reales ====== .requestCache(rc -> { HttpSessionRequestCache cache = new HttpSessionRequestCache(); diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 2014254..321a344 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -1,5 +1,9 @@ spring.application.name=erp +server.forward-headers-strategy=framework +server.servlet.session.cookie.secure=true + + # # Logging # diff --git a/src/main/resources/i18n/cart_es.properties b/src/main/resources/i18n/cart_es.properties index 19f0a27..d8b52b3 100644 --- a/src/main/resources/i18n/cart_es.properties +++ b/src/main/resources/i18n/cart_es.properties @@ -7,6 +7,17 @@ cart.precio=Precio cart.tabs.details=Detalles cart.tabs.envio=Envío +cart.shipping.add=Añadir dirección +cart.shipping.add.title=Seleccione una dirección +cart.shipping.select-placeholder=Buscar en direcciones... +cart.shipping.new-address=Nueva dirección +cart.shipping.info=Todos los pedidos incluyen un envío gratuito a la Península y Baleares por línea de pedido. +cart.shipping.order=Envío del pedido +cart.shipping.samples=Envío de pruebas +cart.shipping.onlyOneShipping=Todo el pedido se envía a una única dirección. + +cart.shipping.errors.noAddressSelected=Debe seleccionar una dirección de envío para el pedido. + cart.resumen.title=Resumen de la cesta cart.resumen.base=Base imponible: cart.resumen.iva-4=IVA 4%: diff --git a/src/main/resources/i18n/pedidos_es.properties b/src/main/resources/i18n/pedidos_es.properties index c23bb54..7cbfc47 100644 --- a/src/main/resources/i18n/pedidos_es.properties +++ b/src/main/resources/i18n/pedidos_es.properties @@ -7,10 +7,7 @@ checkout.shipping.info=Todos los pedidos incluyen un envío gratuito a la Penín 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 diff --git a/src/main/resources/static/assets/css/cart.css b/src/main/resources/static/assets/css/cart.css new file mode 100644 index 0000000..dd4d314 --- /dev/null +++ b/src/main/resources/static/assets/css/cart.css @@ -0,0 +1,33 @@ +.nav-pills .nav-link.active, .nav-pills .show > .nav-link { + background-color: #4c5c6366 !important; +} + +.step-arrow-nav .nav { + background-color: #4c5c6333 !important; +} + +.nav-link.active .bg-soft-primary { + background-color: #ffffff33 !important; + /* #4c5c63 al 20% */ +} + +.nav-link.active .text-primary { + color: #ffffff !important; + /* #4c5c63 al 20% */ +} + +.nav-link:not(.active) .bg-soft-primary { + background-color: #4c5c6366 !important; + /* #4c5c63 al 20% */ +} + +.nav-link:not(.active) .text-primary { + color: #000000 !important; + /* #4c5c63 al 20% */ +} + +.direccion-card { + flex: 1 1 250px; /* ancho mínimo 250px, crece si hay espacio */ + max-width: 250px; /* opcional, para que no se estiren demasiado */ + min-width: 240px; /* protege el ancho mínimo */ +} \ No newline at end of file diff --git a/src/main/resources/static/assets/js/pages/imprimelibros/cart/shipping-cart.js b/src/main/resources/static/assets/js/pages/imprimelibros/cart/shipping-cart.js index e0ef8de..52a8d1a 100644 --- a/src/main/resources/static/assets/js/pages/imprimelibros/cart/shipping-cart.js +++ b/src/main/resources/static/assets/js/pages/imprimelibros/cart/shipping-cart.js @@ -1,27 +1,214 @@ $(() => { - 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); - } - }); - } + $("#onlyOneShipping").on('change', function () { + if ($(this).is(':checked')) { + $('.nav-product').hide(); + document.querySelectorAll('.card.product').forEach(card => { + const detailsBtn = card.querySelector('.nav-link[id^="pills-details-"][id$="-tab"]'); + if (detailsBtn) new bootstrap.Tab(detailsBtn).show(); + }); + $('#shippingAddressesContainer').empty().show(); + $('.div-shipping-product').show(); + $('#addOrderAddress').show(); + } else { + $('.nav-product').show(); + $('#shippingAddressesContainer').hide(); + $('.div-shipping-product').empty().hide(); + $('#addOrderAddress').hide(); + } + }); + + $('#shippingAddressesContainer').on('click', '.btn-delete-direccion', function (e) { + e.preventDefault(); + const $card = $(this).closest('.direccion-card'); + $card.remove(); + $('#addOrderAddress').show(); + }); 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(); + $(document).on("change", ".direccionFacturacion", function () { + const isChecked = $(this).is(':checked'); + if (isChecked) { + $('.direccionFacturacionItems').removeClass('d-none'); } else { - $('#shippingAddressesContainer').hide(); - $('#shippingMultipleAddressesContainer').empty().hide(); + $('.direccionFacturacionItems').addClass('d-none'); + $('#razonSocial').val(''); + $('#tipoIdentificacionFiscal').val('DNI'); + $('#identificacionFiscal').val(''); } }); + + $('#addOrderAddress').on('click', () => { + if ($('#onlyOneShipping').is(':checked')) { + if(seleccionarDireccionEnvio()){ + $('#addOrderAddress').hide(); + } + } else { + // Add address to multiple shipping addresses container + } + }); + + async function seleccionarDireccionEnvio() { + + const { value: direccionId, isDenied } = await Swal.fire({ + title: window.languageBundle['cart.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['cart.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['cart.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( + window.languageBundle['cart.shipping.errors.noAddressSelected'] || 'Por favor, seleccione una dirección.' + ); + 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(`/cart/get-address/${direccionId}`); + if (response.ok) { + const html = await response.text(); + $('#shippingAddressesContainer').append(html); + return true; + } + return false; + } + return false; + } + + $(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.
'); + } + }); + }); }); \ 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 index ea7c10b..3aa8e86 100644 --- a/src/main/resources/static/assets/js/pages/imprimelibros/checkout/checkout.js +++ b/src/main/resources/static/assets/js/pages/imprimelibros/checkout/checkout.js @@ -15,179 +15,7 @@ $(() => { 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/_cartItem.html b/src/main/resources/templates/imprimelibros/cart/_cartItem.html index 2ab4390..209e36a 100644 --- a/src/main/resources/templates/imprimelibros/cart/_cartItem.html +++ b/src/main/resources/templates/imprimelibros/cart/_cartItem.html @@ -3,27 +3,29 @@ data-iva-21=${item.iva21}, data-base=${item.base}">
-