cargando carrito desde backend

This commit is contained in:
2025-10-29 23:30:33 +01:00
parent ae2904aa71
commit feff9ee94a
23 changed files with 848 additions and 183 deletions

View File

@ -20,6 +20,12 @@ databaseChangeLog:
primaryKey: true
primaryKeyName: pk_cart_direcciones
- column:
name: cart_id
type: BIGINT
constraints:
nullable: false
- column:
name: direccion_id
type: BIGINT
@ -37,6 +43,18 @@ databaseChangeLog:
type: INT
constraints:
nullable: true
- column:
name: is_palets
type: TINYINT(1)
constraints:
nullable: false
defaultValue: false
- column:
name: base
type: DECIMAL(12, 2)
- createIndex:
indexName: idx_cart_dir_direccion_id

View File

@ -21,7 +21,9 @@ cart.shipping.ud=ud.
cart.shipping.uds=uds.
cart.shipping.enter-units=Introduzca el número de unidades para esta dirección:
cart.shipping.units-label=Número de unidades (máximo {max})
cart.shipping.send-in-palets=Enviar en palets
cart.shipping.send-in-palets.info=Marque esta opción si desea que el envío se realice en palets (sólo para tiradas grandes). La entrega se realizará a pie de calle.
cart.shipping.tipo-envio=Tipo de envío:
cart.shipping.errors.units-error=Por favor, introduzca un número válido entre 1 y {max}.
cart.shipping.errors.noAddressSelected=Debe seleccionar una dirección de envío para el pedido.
@ -34,4 +36,6 @@ cart.resumen.iva-21=IVA 21%:
cart.resumen.total=Total cesta:
cart.resumen.tramitar=Tramitar pedido
cart.resumen.fidelizacion=Si tiene descuento por fidelización, se aplicará al tramitar el pedido.
cart.resumen.fidelizacion=Si tiene descuento por fidelización, se aplicará al tramitar el pedido.
cart.errors.update-cart=Error al actualizar la cesta de la compra: {0}

View File

@ -29,3 +29,13 @@ body {
color: #fff;
border-color: #92b2a7;
}
/* Solo dentro del modal */
.swal2-popup .form-switch-custom {
font-size: 1rem; /* clave: fija el tamaño base del switch */
line-height: 1.5;
}
.swal2-popup .form-switch-custom .form-check-input {
float: none; /* por si acaso */
margin: 0;
}

View File

@ -21,7 +21,7 @@ $(() => {
$(this).find('.presupuesto-id').attr('name', 'direcciones[' + i + '].presupuestoId');
if($(this).find('.item-tirada').length > 0 && $(this).find('.item-tirada').val() !== null
&& $(this).find('.item-tirada').val() !== "")
$(this).find('.item-tirada').attr('name', 'direcciones[' + i + '].tirada');
$(this).find('.item-tirada').attr('name', 'direcciones[' + i + '].unidades');
});
$.post(form.attr('action'), form.serialize(), (response) => {
// handle response

View File

@ -7,23 +7,23 @@ $(() => {
$("#onlyOneShipment").on('change', function () {
if ($(this).is(':checked')) {
$('.nav-product').hide();
$('.nav-product').addClass('d-none');
document.querySelectorAll('.card.product').forEach(card => {
const detailsBtn = card.querySelector('.nav-link[id^="pills-details-"][id$="-tab"]');
if (detailsBtn) new bootstrap.Tab(detailsBtn).show();
if (detailsBtn) $(new bootstrap.Tab(detailsBtn)).removeClass('d-none');
});
$('#shippingAddressesContainer').empty().show();
$('#shippingAddressesContainer').empty().removeClass('d-none');
$('.shipping-addresses-item').toArray().forEach(element => {
$(element).empty().hide();
$(element).empty().addClass('d-none');
});
$('#addOrderAddress').show();
$('#addOrderAddress').removeClass('d-none');
} else {
$('.nav-product').show();
$('#shippingAddressesContainer').empty().hide();
$('.nav-product').removeClass('d-none');
$('#shippingAddressesContainer').empty().addClass('d-none');
$('.shipping-addresses-item').toArray().forEach(element => {
$(element).empty().show();
$(element).empty().removeClass('d-none');
});
$('#addOrderAddress').hide();
$('#addOrderAddress').addClass('d-none');
}
$(document).trigger('updateCart');
});
@ -34,7 +34,7 @@ $(() => {
const $div = $card.parent();
$card.remove();
if ($div.hasClass('shipping-order-address')) {
$('#addOrderAddress').show();
$('#addOrderAddress').removeClass('d-none');
}
else {
$div.trigger('direcciones:actualizadas');
@ -50,19 +50,27 @@ $(() => {
const tirada = $(this).closest('.product').find('.item-tirada').val();
const totalTirada = container.find('.item-tirada').toArray().reduce((acc, el) => acc + parseInt($(el).val() || 0), 0);
const remainingTirada = parseInt(tirada) - parseInt(totalTirada) + parseInt($card.find('.item-tirada').val() || 0);
const units = getUnitsFromUser(remainingTirada);
units.then(unidades => {
if (unidades) {
$card.find('.item-tirada').val(unidades);
const data = getUnitsFromUser(remainingTirada);
data.then(data => {
if (data.unidades) {
$card.find('.item-tirada').val(data.unidades);
$card.find('#units-text').each(function () {
if (unidades == 1) {
$(this).text(`${unidades} ${window.languageBundle['cart.shipping.ud'] || 'unidad'}`);
if (data.unidades == 1) {
$(this).text(`${data.unidades} ${window.languageBundle['cart.shipping.ud'] || 'unidad'}`);
} else {
$(this).text(`${unidades} ${window.languageBundle['cart.shipping.uds'] || 'unidades'}`);
$(this).text(`${data.unidades} ${window.languageBundle['cart.shipping.uds'] || 'unidades'}`);
}
});
container.trigger('direcciones:actualizadas');
$(document).trigger('updateCart');
$card.find('.is-palets').val(data.isPalets ? 'true' : 'false');
$card.find('.icon-shipment').each(function () {
if (data.isPalets) {
$(this).removeClass('la-box').addClass('la-pallet');
} else {
$(this).removeClass('la-pallet').addClass('la-box');
}
});
}
});
@ -87,7 +95,7 @@ $(() => {
$('#addOrderAddress').on('click', async () => {
if ($('#onlyOneShipment').is(':checked')) {
if (await seleccionarDireccionEnvio()) {
$('#addOrderAddress').hide();
$('#addOrderAddress').addClass('d-none');
}
}
});
@ -210,16 +218,27 @@ $(() => {
$('#direccionFormModalBody').html(html);
const title = $('#direccionFormModalBody #direccionForm').data('add');
$('#direccionFormModal .modal-title').text(title);
modal.show();
modal.removeClass('d-none');
});
}
let unidades = null;
let isPalets = 0;
if (tirada !== null && tirada >= 1 && direccionId) {
const unidadesValue = await getUnitsFromUser(tirada);
if (unidadesValue) {
unidades = parseInt(unidadesValue);
const data = await getUnitsFromUser(tirada);
if (data && data.unidades) {
unidades = parseInt(data.unidades);
isPalets = data.isPalets ? 1 : 0;
} else {
// Si el usuario cancela, salir de la función
return false;
}
}
else if(presupuestoId == null){ // caso para todas los envios a la misma direccion
const isPaletsValue = await getTipoEnvio();
if (isPaletsValue !== null) {
isPalets = isPaletsValue ? 1 : 0;
} else {
// Si el usuario cancela, salir de la función
return false;
@ -228,9 +247,9 @@ $(() => {
if (direccionId) {
// Obtén el objeto completo seleccionado
showLoader();
let uri = `/cart/get-address/${direccionId}`;
let uri = `/cart/get-address/${direccionId}?isPalets=${isPalets}`;
if (presupuestoId !== null) {
uri += `?presupuestoId=${presupuestoId}`;
uri += `&presupuestoId=${presupuestoId}`;
if (tirada !== null) {
uri += `&unidades=${unidades}`;
}
@ -255,18 +274,29 @@ $(() => {
}
async function getUnitsFromUser(tirada) {
// Swal preguntando numero de unidades a asignar con máximo de tirada, necesito guardar el valor
const { value: unidadesValue } = await Swal.fire({
const { value: formValues } = await Swal.fire({
title: window.languageBundle['cart.shipping.enter-units'] || 'Introduzca el número de unidades para esta dirección',
input: 'number',
inputLabel: window.languageBundle['cart.shipping.units-label']?.replace('{max}', tirada) || `Número de unidades (máximo ${tirada})`,
inputAttributes: {
min: 1,
max: tirada,
step: 1,
},
inputValue: tirada,
html: `
<div class="mb-3">
<label class="form-label fw-semibold">
${window.languageBundle['cart.shipping.units-label']?.replace('{max}', tirada) || `Número de unidades (máximo ${tirada})`}
</label>
<input id="swal-input-unidades" type="number" min="1" max="${tirada}" step="1"
value="${tirada}" class="form-control text-center">
</div>
<div class="form-check form-switch form-switch-custom mb-3 d-flex align-items-center justify-content-center ps-0">
<input type="checkbox"
id="swal-input-palets"
class="form-check-input ms-0 me-2 float-none">
<label for="swal-input-palets" class="form-check-label mb-0">
${window.languageBundle['cart.shipping.send-in-palets'] || 'Enviar en palets'}
</label>
</div>
<span class="form-text text-muted">
${window.languageBundle['cart.shipping.send-in-palets.info'] || 'En palets la entrega se realizará a pie de calle.'}
</span>
`,
focusConfirm: false,
showCancelButton: true,
buttonsStyling: false,
customClass: {
@ -275,14 +305,60 @@ $(() => {
},
confirmButtonText: window.languageBundle['app.aceptar'] || 'Aceptar',
cancelButtonText: window.languageBundle['app.cancelar'] || 'Cancelar',
inputValidator: (value) => {
if (!value || isNaN(value) || value < 1 || value > tirada) {
return window.languageBundle['cart.shipping.errors.units-error']?.replace('{max}', tirada) || `Por favor, introduzca un número válido entre 1 y ${tirada}.`;
preConfirm: () => {
const unidades = parseInt(document.getElementById('swal-input-unidades').value, 10);
const isPalets = document.getElementById('swal-input-palets').checked;
if (!unidades || isNaN(unidades) || unidades < 1 || unidades > tirada) {
Swal.showValidationMessage(
window.languageBundle['cart.shipping.errors.units-error']?.replace('{max}', tirada)
|| `Por favor, introduzca un número válido entre 1 y ${tirada}.`
);
return false;
}
return null;
return { unidades, isPalets };
}
});
return unidadesValue;
if (formValues) {
return formValues; // { unidades: number, isPalets: boolean }
}
return null; // Si se cancela el Swal
}
async function getTipoEnvio() {
const { value: checkValue } = await Swal.fire({
title: window.languageBundle['cart.shipping.tipo-envio'] || 'Tipo de envío',
html: `
<div class="form-check form-switch form-switch-custom my-3 d-flex align-items-center justify-content-center gap-2">
<input type="checkbox" class="form-check-input" id="swal-input-palets">
<label for="swal-input-palets" class="form-label mb-0">
${window.languageBundle['cart.shipping.send-in-palets'] || 'Enviar en palets'}
</label>
</div>
<span class="form-text text-muted">
${window.languageBundle['cart.shipping.send-in-palets.info'] || 'En palets la entrega se realizará a pie de calle.'}
</span>
`,
focusConfirm: false,
showCancelButton: true,
buttonsStyling: false,
customClass: {
confirmButton: 'btn btn-secondary me-2',
cancelButton: 'btn btn-light',
},
confirmButtonText: window.languageBundle['app.aceptar'] || 'Aceptar',
cancelButtonText: window.languageBundle['app.cancelar'] || 'Cancelar',
preConfirm: () => {
const isPalets = document.getElementById('swal-input-palets').checked;
return isPalets;
}
});
if (checkValue !== undefined) {
return checkValue; // boolean
}
return null; // Si se cancela el Swal
}
function checkTotalUnits(container, tirada) {
@ -294,8 +370,8 @@ $(() => {
if (totalUnits < tirada) {
return false;
}
if(container.find('.product').closest('.shipping-addresses-sample')){
if(container.find('.direccion-card').toArray().length === 0){
if (container.find('.product').closest('.shipping-addresses-sample')) {
if (container.find('.direccion-card').toArray().length === 0) {
return false;
}
}
@ -308,21 +384,21 @@ $(() => {
const container = $(this);
if (!checkTotalUnits(container, tirada)) {
container.closest('.px-2').find('.btn-add-shipping').show();
container.closest('.px-2').find('.btn-add-shipping').removeClass('d-none');
} else {
container.closest('.px-2').find('.btn-add-shipping').hide();
container.closest('.px-2').find('.btn-add-shipping').addClass('d-none');
}
});
$(document).on('direcciones:actualizadas', '.shipping-addresses-sample', function (e) {
const container = $(this);
if (container.find('.direccion-card').toArray().length === 0) {
container.closest('.px-2').find('.btn-add-shipping-sample').show();
container.closest('.px-2').find('.btn-add-shipping-sample').removeClass('d-none');
}
else {
container.closest('.px-2').find('.btn-add-shipping-sample').hide();
container.closest('.px-2').find('.btn-add-shipping-sample').addClass('d-none');
}
});
@ -345,7 +421,7 @@ $(() => {
return;
}
// Éxito real: cerrar y recargar tabla
modal.hide();
modal.addClass('d-none');
seleccionarDireccionEnvio();
},
error: function (xhr) {

View File

@ -10,26 +10,28 @@
<div th:if="${items.isEmpty()}">
<div class="alert alert-info" role="alert" th:text="#{cart.empty}"></div>
</div>
<div th:if="${errorMessage}" class="alert alert-danger " role="alert" th:text="${errorMessage}"></div>
<div class="alert alert-danger alert-shipment d-none" role="alert"
th:text="#{cart.shipping.errors.fillAddressesItems}"></div>
<form id="cartForm" th:action="${'/cart/update/' + cartId}" method="POST" class="col-xl-8 col-12">
<form id="cartForm" th:action="${'/cart/update/' + cart.id}" method="POST" class="col-xl-8 col-12">
<input type="hidden" name="id" th:value="${cartId}" />
<input type="hidden" name="id" th:value="${cart.id}" />
<div class="card">
<div class="card-body">
<p th:text="#{cart.shipping.info}"></p>
<div
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" id="onlyOneShipment"
name="only_one_shipment" checked />
th:field="${cart.onlyOneShipment}"/>
<label for="onlyOneShipment" class="form-label d-flex align-items-center mb-0">
<span th:text="#{cart.shipping.onlyOneShipment}" class="me-2"></span>
</label>
</div>
<button type="button" class="btn btn-secondary" id="addOrderAddress"
<button type="button" th:class="${'btn btn-secondary' + (!cart.onlyOneShipment or mainDir?.size > 0 ? ' d-none' : '')}" id="addOrderAddress"
th:text="#{cart.shipping.add}">Añadir dirección</button>
<div id="shippingAddressesContainer" class="shipping-order-address d-flex flex-wrap gap-3 mt-4"></div>

View File

@ -8,7 +8,7 @@
<input type="hidden" class="item-tirada" th:value="${item.tirada}" />
<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">
<ul th:class="${'nav nav-pills nav-justified custom-nav nav-product' + (cart.onlyOneShipment ? ' d-none' : '')}" role="tablist">
<li class="nav-item" role="presentation">
<button class="nav-link fs-15 active" th:id="${'pills-details-' + item.cartItemId + '-tab'}"
th:data-bs-target="${'#pills-details-' + item.cartItemId}" type="button" role="tab"

View File

@ -7,6 +7,7 @@
<input type="hidden" class="direccion-cp" th:value="${direccion.cp}" />
<input type="hidden" class="direccion-pais-code3" th:value="${direccion.pais.code3}" />
<input type="hidden" class="item-tirada" th:value="${unidades != null ? unidades : ''}" />
<input type="hidden" class="is-palets" th:value="${isPalets != null ? isPalets : ''}" />
<div class="row g-3 align-items-start flex-nowrap">
<div class="col">
@ -17,17 +18,24 @@
<span class="text-muted fw-normal d-block text-break"
th:text="#{'direcciones.telefono'} + ': ' + ${direccion.telefono}"></span>
</div>
<div th:if="${unidades != null}" class="col-auto ms-auto text-end">
<span id="units-text" class="mb-2 fw-semibold d-block text-muted text-uppercase" th:if="${unidades == 1}"
th:text="|${unidades} #{cart.shipping.ud}|"></span>
<div class="col-auto ms-auto text-end">
<div th:if="${unidades != null}">
<span id="units-text" class="mb-2 fw-semibold d-block text-muted text-uppercase" th:if="${unidades == 1}"
th:text="|${unidades} #{cart.shipping.ud}|"></span>
<!-- plural -->
<span id="units-text" class="mb-2 fw-semibold d-block text-muted text-uppercase" th:unless="${unidades == 1}"
th:text="|${unidades} #{cart.shipping.uds}|"></span>
<!-- plural -->
<span id="units-text" class="mb-2 fw-semibold d-block text-muted text-uppercase" th:unless="${unidades == 1}"
th:text="|${unidades} #{cart.shipping.uds}|"></span>
</div>
<div th:if="${isPalets != null and isPalets==1}">
<i class="icon-shipment las la-pallet la-3x text-muted"></i>
</div>
<div th:if="${isPalets == null or isPalets == 0}">
<i class="icon-shipment las la-box la-3x text-muted"></i>
</div>
</div>
</div>
<div
class="d-flex flex-wrap align-items-center gap-2 py-1 bg-light rounded-bottom border-top mt-auto actions-row">
<div class="d-flex flex-wrap align-items-center gap-2 py-1 bg-light rounded-bottom border-top mt-auto actions-row">
<a href="javascript:void(0)" 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>

View File

@ -0,0 +1,7 @@
<form id="tpv" th:action="${action}" method="POST">
<input type="hidden" name="Ds_SignatureVersion" th:value="${signatureVersion}" />
<input type="hidden" name="Ds_MerchantParameters" th:value="${merchantParameters}" />
<input type="hidden" name="Ds_Signature" th:value="${signature}" />
<noscript><button type="submit">Pagar</button></noscript>
</form>
<script>document.getElementById('tpv').submit();</script>