mirror of
https://git.imnavajas.es/jjimenez/erp-imprimelibros.git
synced 2026-01-13 00:48:49 +00:00
direccion unica añadida
This commit is contained in:
@ -3,13 +3,20 @@ package com.imprimelibros.erp.cart;
|
|||||||
import org.springframework.stereotype.Controller;
|
import org.springframework.stereotype.Controller;
|
||||||
import org.springframework.ui.Model;
|
import org.springframework.ui.Model;
|
||||||
import org.springframework.web.bind.annotation.*;
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
import org.springframework.web.server.ResponseStatusException;
|
||||||
|
|
||||||
import jakarta.servlet.http.HttpServletRequest;
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
|
|
||||||
|
import org.springframework.context.MessageSource;
|
||||||
|
import org.springframework.http.HttpStatus;
|
||||||
import org.springframework.http.ResponseEntity;
|
import org.springframework.http.ResponseEntity;
|
||||||
import com.imprimelibros.erp.common.Utils;
|
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.security.Principal;
|
||||||
|
import java.util.List;
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
@ -17,18 +24,38 @@ import java.util.Map;
|
|||||||
@RequestMapping("/cart")
|
@RequestMapping("/cart")
|
||||||
public class CartController {
|
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.service = service;
|
||||||
|
this.direccionService = direccionService;
|
||||||
|
this.messageSource = messageSource;
|
||||||
|
this.translationService = translationService;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/** Vista del carrito */
|
/** Vista del carrito */
|
||||||
@GetMapping
|
@GetMapping
|
||||||
public String viewCart(Model model, Principal principal, Locale locale) {
|
public String viewCart(Model model, Principal principal, Locale locale) {
|
||||||
|
|
||||||
|
List<String> 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<String, String> translations = translationService.getTranslations(locale, keys);
|
||||||
|
model.addAttribute("languageBundle", translations);
|
||||||
|
|
||||||
var items = service.listItems(Utils.currentUserId(principal), locale);
|
var items = service.listItems(Utils.currentUserId(principal), locale);
|
||||||
model.addAttribute("items", items);
|
model.addAttribute("items", items);
|
||||||
|
model.addAttribute("cartId", service.getOrCreateActiveCart(Utils.currentUserId(principal)));
|
||||||
return "imprimelibros/cart/cart"; // crea esta vista si quieres (tabla simple)
|
return "imprimelibros/cart/cart"; // crea esta vista si quieres (tabla simple)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -82,4 +109,15 @@ public class CartController {
|
|||||||
service.clear(Utils.currentUserId(principal));
|
service.clear(Utils.currentUserId(principal));
|
||||||
return "redirect:/cart";
|
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})";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -72,15 +72,4 @@ public class CheckoutController {
|
|||||||
model.addAttribute("items", items);
|
model.addAttribute("items", items);
|
||||||
return "imprimelibros/checkout/checkout"; // crea esta vista si quieres (tabla simple)
|
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})";
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -91,7 +91,6 @@ public class SecurityConfig {
|
|||||||
// Ignora CSRF para tu recurso público (sin Ant/Mvc matchers)
|
// Ignora CSRF para tu recurso público (sin Ant/Mvc matchers)
|
||||||
.csrf(csrf -> csrf
|
.csrf(csrf -> csrf
|
||||||
.ignoringRequestMatchers(pathStartsWith("/presupuesto/public/")))
|
.ignoringRequestMatchers(pathStartsWith("/presupuesto/public/")))
|
||||||
|
|
||||||
// ====== RequestCache: sólo navegaciones HTML reales ======
|
// ====== RequestCache: sólo navegaciones HTML reales ======
|
||||||
.requestCache(rc -> {
|
.requestCache(rc -> {
|
||||||
HttpSessionRequestCache cache = new HttpSessionRequestCache();
|
HttpSessionRequestCache cache = new HttpSessionRequestCache();
|
||||||
|
|||||||
@ -1,5 +1,9 @@
|
|||||||
spring.application.name=erp
|
spring.application.name=erp
|
||||||
|
|
||||||
|
server.forward-headers-strategy=framework
|
||||||
|
server.servlet.session.cookie.secure=true
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# Logging
|
# Logging
|
||||||
#
|
#
|
||||||
|
|||||||
@ -7,6 +7,17 @@ cart.precio=Precio
|
|||||||
cart.tabs.details=Detalles
|
cart.tabs.details=Detalles
|
||||||
cart.tabs.envio=Envío
|
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.title=Resumen de la cesta
|
||||||
cart.resumen.base=Base imponible:
|
cart.resumen.base=Base imponible:
|
||||||
cart.resumen.iva-4=IVA 4%:
|
cart.resumen.iva-4=IVA 4%:
|
||||||
|
|||||||
@ -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.order=Envío del pedido
|
||||||
checkout.shipping.samples=Envío de pruebas
|
checkout.shipping.samples=Envío de pruebas
|
||||||
checkout.shipping.onlyOneShipping=Todo el pedido se envía a una única dirección.
|
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.presupuesto=#Presupuesto
|
||||||
checkout.summary.titulo=Título
|
checkout.summary.titulo=Título
|
||||||
|
|||||||
33
src/main/resources/static/assets/css/cart.css
Normal file
33
src/main/resources/static/assets/css/cart.css
Normal file
@ -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 */
|
||||||
|
}
|
||||||
@ -1,27 +1,214 @@
|
|||||||
$(() => {
|
$(() => {
|
||||||
|
|
||||||
const csrfToken = document.querySelector('meta[name="_csrf"]')?.getAttribute('content');
|
$("#onlyOneShipping").on('change', function () {
|
||||||
const csrfHeader = document.querySelector('meta[name="_csrf_header"]')?.getAttribute('content');
|
if ($(this).is(':checked')) {
|
||||||
if (window.$ && csrfToken && csrfHeader) {
|
$('.nav-product').hide();
|
||||||
$.ajaxSetup({
|
document.querySelectorAll('.card.product').forEach(card => {
|
||||||
beforeSend: function (xhr) {
|
const detailsBtn = card.querySelector('.nav-link[id^="pills-details-"][id$="-tab"]');
|
||||||
xhr.setRequestHeader(csrfHeader, csrfToken);
|
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 language = document.documentElement.lang || 'es-ES';
|
||||||
const modalEl = document.getElementById('direccionFormModal');
|
const modalEl = document.getElementById('direccionFormModal');
|
||||||
const modal = bootstrap.Modal.getOrCreateInstance(modalEl);
|
const modal = bootstrap.Modal.getOrCreateInstance(modalEl);
|
||||||
|
|
||||||
|
$(document).on("change", ".direccionFacturacion", function () {
|
||||||
$("#onlyOneShipping").on('change', function () {
|
const isChecked = $(this).is(':checked');
|
||||||
if ($(this).is(':checked')) {
|
if (isChecked) {
|
||||||
$('#shippingAddressesContainer').empty().show();
|
$('.direccionFacturacionItems').removeClass('d-none');
|
||||||
$('#shippingMultipleAddressesContainer').show();
|
|
||||||
} else {
|
} else {
|
||||||
$('#shippingAddressesContainer').hide();
|
$('.direccionFacturacionItems').addClass('d-none');
|
||||||
$('#shippingMultipleAddressesContainer').empty().hide();
|
$('#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: `
|
||||||
|
<select id="direccionSelect" class="form-select" style="width: 100%"></select>
|
||||||
|
`,
|
||||||
|
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: `
|
||||||
|
<div>
|
||||||
|
<strong>${item.alias || 'Sin alias'}</strong><br>
|
||||||
|
${item.att ? `<small>${item.att}</small><br>` : ''}
|
||||||
|
<small>${item.direccion || ''}${item.cp ? ', ' + item.cp : ''}${item.ciudad ? ', ' + item.ciudad : ''}</small>
|
||||||
|
</div>
|
||||||
|
`
|
||||||
|
})),
|
||||||
|
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 $(`<span>${alias}${ciudad}</span>`);
|
||||||
|
},
|
||||||
|
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('<html') === -1) {
|
||||||
|
$('#direccionFormModalBody').html(html);
|
||||||
|
const isEdit = $('#direccionFormModalBody #direccionForm input[name="_method"][value="PUT"]').length > 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('<div class="p-3 text-danger">Error inesperado.</div>');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
@ -15,179 +15,7 @@ $(() => {
|
|||||||
const modal = bootstrap.Modal.getOrCreateInstance(modalEl);
|
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: `
|
|
||||||
<select id="direccionSelect" class="form-select" style="width: 100%"></select>
|
|
||||||
`,
|
|
||||||
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: `
|
|
||||||
<div>
|
|
||||||
<strong>${item.alias || 'Sin alias'}</strong><br>
|
|
||||||
${item.att ? `<small>${item.att}</small><br>` : ''}
|
|
||||||
<small>${item.direccion || ''}${item.cp ? ', ' + item.cp : ''}${item.ciudad ? ', ' + item.ciudad : ''}</small>
|
|
||||||
</div>
|
|
||||||
`
|
|
||||||
})),
|
|
||||||
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 $(`<span>${alias}${ciudad}</span>`);
|
|
||||||
},
|
|
||||||
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('<html') === -1) {
|
|
||||||
$('#direccionFormModalBody').html(html);
|
|
||||||
const isEdit = $('#direccionFormModalBody #direccionForm input[name="_method"][value="PUT"]').length > 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('<div class="p-3 text-danger">Error inesperado.</div>');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|||||||
@ -3,27 +3,29 @@
|
|||||||
data-iva-21=${item.iva21},
|
data-iva-21=${item.iva21},
|
||||||
data-base=${item.base}">
|
data-base=${item.base}">
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<ul class="nav nav-pills nav-justified custom-nav nav-product" role="tablist">
|
<div class="step-arrow-nav mt-n3 mx-n3 mb-3">
|
||||||
|
<ul class="nav nav-pills nav-justified custom-nav nav-product" style="display: none;" role="tablist">
|
||||||
<li class="nav-item" role="presentation">
|
<li class="nav-item" role="presentation">
|
||||||
<button class="nav-link fs-15 active" id="pills-details-${item.id}-tab"
|
<button class="nav-link fs-15 active" th:id="${'pills-details-' + item.cartItemId + '-tab'}"
|
||||||
data-bs-target="#pills-details-${item.id}" type="button" role="tab"
|
th:data-bs-target="${'#pills-details-' + item.cartItemId}" type="button" role="tab"
|
||||||
aria-controls="pills-details-${item.id}" aria-selected="true">
|
th:aria-controls="${'#pills-details-' + item.cartItemId}" aria-selected="true" data-bs-toggle="tab">
|
||||||
<i class="ri-truck-line fs-5 p-1 bg-soft-primary text-primary rounded-circle align-middle me-2"></i>
|
<i class="ri-truck-line fs-5 p-1 bg-soft-primary text-primary rounded-circle align-middle me-2"></i>
|
||||||
<label class="fs-13 my-2" th:text="#{cart.tabs.details}">Detalles</label>
|
<label class="fs-13 my-2" th:text="#{cart.tabs.details}">Detalles</label>
|
||||||
</button>
|
</button>
|
||||||
</li>
|
</li>
|
||||||
<li class="nav-item" role="presentation">
|
<li class="nav-item" role="presentation">
|
||||||
<button class="nav-link fs-15 p-3" id="pills-shipping-${item.id}-tab"
|
<button class="nav-link fs-15" th:id="${'pills-shipping-' + item.cartItemId + '-tab'}"
|
||||||
data-bs-target="#pills-shipping-${item.id}" type="button" role="tab"
|
th:data-bs-target="${'#pills-shipping-' + item.cartItemId}" type="button" role="tab"
|
||||||
aria-controls="pills-shipping-${item.id}" aria-selected="false">
|
th:aria-controls="${'#pills-shipping-' + item.cartItemId}" aria-selected="false" data-bs-toggle="tab">
|
||||||
<i class="ri-truck-line fs-5 p-1 bg-soft-primary text-primary rounded-circle align-middle me-2"></i>
|
<i class="ri-truck-line fs-5 p-1 bg-soft-primary text-primary rounded-circle align-middle me-2"></i>
|
||||||
<label class="fs-13 my-2" th:text="#{cart.tabs.envio}">Envío</label>
|
<label class="fs-13 my-2" th:text="#{cart.tabs.envio}">Envío</label>
|
||||||
</button>
|
</button>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
</div>
|
||||||
<div class="tab-content row gy-3">
|
<div class="tab-content row gy-3">
|
||||||
<div class="tab-pane fade show active" id="pills-details-${item.id}" role="tabpanel"
|
<div class="tab-pane fade show tab-pane-details active" th:id="${'pills-details-' + item.cartItemId}" role="tabpanel"
|
||||||
aria-labelledby="pills-details-${item.id}-tab">
|
th:aria-labelledby="${'pills-details-' + item.cartItemId + '-tab'}">
|
||||||
<div class="col-sm-auto">
|
<div class="col-sm-auto">
|
||||||
<div class="avatar-lg bg-light rounded p-1">
|
<div class="avatar-lg bg-light rounded p-1">
|
||||||
<img th:src="${item.imagen != null ? item.imagen : '/assets/images/products/placeholder.png'}"
|
<img th:src="${item.imagen != null ? item.imagen : '/assets/images/products/placeholder.png'}"
|
||||||
@ -89,9 +91,24 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="tab-content">
|
<div class="tab-content">
|
||||||
<div class="tab-pane fade show active" id="pills-shipping-${item.id}" role="tabpanel"
|
<div class="tab-pane fade show tab-pane-shipping" th:id="${'pills-shipping-' + item.cartItemId}" role="tabpanel"
|
||||||
aria-labelledby="pills-shipping-${item.id}-tab">
|
th:aria-labelledby="${'pills-shipping-' + item.cartItemId + '-tab'}">
|
||||||
|
|
||||||
|
<div class="col-sm">
|
||||||
|
<!-- Título / enlace -->
|
||||||
|
<h5 class="fs-18 text-truncate my-1 p-1">
|
||||||
|
<a th:href="@{|presupuesto/edit/${item.presupuestoId}|}" class="text-dark"
|
||||||
|
th:text="${item.titulo != null ? item.titulo : 'Presupuesto #'}">
|
||||||
|
Presupuesto
|
||||||
|
</a>
|
||||||
|
</h5>
|
||||||
|
<h5 class="fs-14 text-truncate mb-1">
|
||||||
|
<span th:text="#{cart.item.presupuesto-numero}">Presupuesto #</span>
|
||||||
|
<span th:text="${item.presupuestoId != null ? item.presupuestoId : ''}">#</span>
|
||||||
|
</h5>
|
||||||
|
</div>
|
||||||
|
<div class="col-sm-auto div-shipping-product">
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -9,7 +9,7 @@
|
|||||||
<link th:href="@{/assets/libs/datatables/dataTables.bootstrap5.min.css}" rel="stylesheet" />
|
<link th:href="@{/assets/libs/datatables/dataTables.bootstrap5.min.css}" rel="stylesheet" />
|
||||||
</th:block>
|
</th:block>
|
||||||
<th:block layout:fragment="pagecss">
|
<th:block layout:fragment="pagecss">
|
||||||
<link th:href="@{/assets/css/presupuestador.css}" rel="stylesheet" />
|
<link th:href="@{/assets/css/cart.css}" rel="stylesheet" />
|
||||||
</th:block>
|
</th:block>
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
@ -22,6 +22,10 @@
|
|||||||
<th:block layout:fragment="content">
|
<th:block layout:fragment="content">
|
||||||
<div th:if="${#authorization.expression('isAuthenticated()')}">
|
<div th:if="${#authorization.expression('isAuthenticated()')}">
|
||||||
|
|
||||||
|
<div
|
||||||
|
th:replace="imprimelibros/partials/modal-form :: modal('direccionFormModal', 'direcciones.add', 'modal-md', 'direccionFormModalBody')">
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="container-fluid">
|
<div class="container-fluid">
|
||||||
<nav aria-label="breadcrumb">
|
<nav aria-label="breadcrumb">
|
||||||
<ol class="breadcrumb">
|
<ol class="breadcrumb">
|
||||||
@ -37,30 +41,32 @@
|
|||||||
<div class="alert alert-info" role="alert" th:text="#{cart.empty}"></div>
|
<div class="alert alert-info" role="alert" th:text="#{cart.empty}"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<form id="cartForm" th:action="${'/cart/update/' + cartId}" method="POST" class="col-xl-8 col-12">
|
||||||
|
|
||||||
|
<input type="hidden" name="id" th:value="${cartId}" />
|
||||||
|
|
||||||
<div class="col-xl-8 col-12">
|
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<p th:text="#{checkout.shipping.info}"></p>
|
<p th:text="#{cart.shipping.info}"></p>
|
||||||
<div
|
<div
|
||||||
class="form-check form-switch form-switch-custom form-switch-presupuesto mb-3 d-flex align-items-center">
|
class="form-check form-switch form-switch-custom form-switch-presupuesto mb-3 d-flex align-items-center">
|
||||||
<input type="checkbox" class="form-check-input datos-generales-data me-2"
|
<input type="checkbox" class="form-check-input datos-generales-data me-2"
|
||||||
id="onlyOneShipping" name="onlyOneShipping" checked />
|
id="onlyOneShipping" name="onlyOneShipping" checked />
|
||||||
<label for="onlyOneShipping" class="form-label d-flex align-items-center mb-0">
|
<label for="onlyOneShipping" class="form-label d-flex align-items-center mb-0">
|
||||||
<span th:text="#{checkout.shipping.onlyOneShipping}" class="me-2"></span>
|
<span th:text="#{cart.shipping.onlyOneShipping}" class="me-2"></span>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<button type="button" class="btn btn-secondary" id="addOrderAddress"
|
<button type="button" class="btn btn-secondary" id="addOrderAddress"
|
||||||
th:text="#{checkout.shipping.add}">Añadir dirección</button>
|
th:text="#{cart.shipping.add}">Añadir dirección</button>
|
||||||
|
|
||||||
<div id="shippingAddressesContainer" class="mt-4"></div>
|
<div id="shippingAddressesContainer" class="d-flex flex-wrap gap-3 mt-4"></div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div th:each="item : ${items}" th:insert="~{imprimelibros/cart/_cartItem :: cartItem(${item})}">
|
<div th:each="item : ${items}" th:insert="~{imprimelibros/cart/_cartItem :: cartItem(${item})}">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</form>
|
||||||
|
|
||||||
<div class="col-xl-4">
|
<div class="col-xl-4">
|
||||||
<div class="sticky-side-div">
|
<div class="sticky-side-div">
|
||||||
@ -114,6 +120,7 @@
|
|||||||
<!-- end stickey -->
|
<!-- end stickey -->
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</th:block>
|
</th:block>
|
||||||
@ -125,6 +132,7 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<script type="module" th:src="@{/assets/js/pages/imprimelibros/cart/cart.js}"></script>
|
<script type="module" th:src="@{/assets/js/pages/imprimelibros/cart/cart.js}"></script>
|
||||||
|
<script type="module" th:src="@{/assets/js/pages/imprimelibros/cart/shipping-cart.js}"></script>
|
||||||
</th:block>
|
</th:block>
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
|
|||||||
@ -1,18 +1,23 @@
|
|||||||
<div th:fragment="direccionCard(direccion)">
|
<div th:fragment="direccionCard(direccion)" class="card card border mb-3 direccion-card bg-light w-100 mx-2" th:attr="data-id=${direccion.id},
|
||||||
<div class="card mb-3 direccion-card"
|
|
||||||
th:attr="data-id=${direccion.id},
|
|
||||||
data-cp=${direccion.cp},
|
data-cp=${direccion.cp},
|
||||||
data-pais-code3=${direccion.pais.code3}">
|
data-pais-code3=${direccion.pais.code3}" >
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<h5 class="card-title" th:text="${direccion.alias}">Alias</h5>
|
|
||||||
|
|
||||||
<h3 class="card-text mb-0"
|
<div class="p-2 mx-3">
|
||||||
th:if="${direccion.att != null and !#strings.isEmpty(direccion.att)}"
|
<span class="mb-2 fw-semibold d-block text-muted text-uppercase" th:text="${direccion.alias}"></span>
|
||||||
th:text="${direccion.att}">Att</h3>
|
<span class="fs-14 mb-1 d-block" th:text="${direccion.att}"></span>
|
||||||
|
<span class="text-muted fw-normal text-wrap mb-1 d-block" th:text="${direccion.direccion}"></span>
|
||||||
<p class="card-text mb-0" th:text="${direccion.direccion}">Calle</p>
|
<span class="text-muted fw-normal d-block" th:text="${pais}"></span>
|
||||||
<p class="card-text mb-0" th:text="${direccion.cp} + ' ' + ${direccion.ciudad}">CP Ciudad</p>
|
<span class="text-muted fw-normal d-block"
|
||||||
<p class="card-text mb-0" th:text="${pais}">País</p>
|
th:text="#{'direcciones.telefono'} + ': ' + ${direccion.telefono}"></span>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="d-flex flex-wrap align-items-center gap-2 px-2 py-1 bg-light rounded-bottom border-top mt-auto actions-row">
|
||||||
|
<a href="#" class="d-block text-body p-1 px-2 btn-delete-direccion" data-id="${this._esc(d.id ?? '')}">
|
||||||
|
<i class="ri-delete-bin-fill text-muted align-bottom me-1"></i>
|
||||||
|
<span th:text="#{'direcciones.btn.delete'}">Eliminar</span>
|
||||||
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
</div>
|
||||||
Reference in New Issue
Block a user