a falta del pago

This commit is contained in:
2025-11-02 11:57:05 +01:00
parent 51d22515e8
commit 4d451cc85e
17 changed files with 429 additions and 208 deletions

View File

@ -291,6 +291,7 @@ public class CartService {
summary.put("fidelizacion", fidelizacion + "%");
summary.put("descuento", Utils.formatCurrency(-descuento, locale));
summary.put("total", Utils.formatCurrency(total, locale));
summary.put("amountCents", Math.round(total * 100));
summary.put("errorShipmentCost", errorShipementCost);
return summary;

View File

@ -6,17 +6,20 @@ import java.util.Locale;
import java.util.Map;
import org.springframework.context.MessageSource;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
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.server.ResponseStatusException;
import com.imprimelibros.erp.common.Utils;
import com.imprimelibros.erp.i18n.TranslationService;
import com.imprimelibros.erp.paises.PaisesService;
import com.imprimelibros.erp.direcciones.Direccion;
import com.imprimelibros.erp.direcciones.DireccionService;
import com.imprimelibros.erp.cart.Cart;
import com.imprimelibros.erp.cart.CartService;
@Controller
@ -44,23 +47,29 @@ public class CheckoutController {
List<String> keys = List.of(
"app.cancelar",
"app.seleccionar",
"checkout.shipping.add.title",
"checkout.shipping.select-placeholder",
"checkout.shipping.new-address",
"app.yes",
"app.cancelar");
"checkout.billing-address.title",
"checkout.billing-address.new-address",
"checkout.billing-address.select-placeholder",
"checkout.billing-address.errors.noAddressSelected");
Map<String, String> 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);
Long userId = Utils.currentUserId(principal);
Cart cart = cartService.getOrCreateActiveCart(userId);
model.addAttribute("summary", cartService.getCartSummary(cart, locale));
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/direccionBillingCard :: direccionBillingCard(direccion=${direccion}, pais=${pais})";
}
}

View File

@ -506,6 +506,29 @@ public class DireccionController {
}
@GetMapping(value = "/facturacion/select2", produces = "application/json")
@ResponseBody
public Map<String, Object> getSelect2Facturacion(
@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 (!isAdmin) {
if (auth != null && auth.getPrincipal() instanceof UserDetailsImpl udi) {
currentUserId = udi.getId();
} else if (auth != null) {
currentUserId = userRepo.findIdByUserNameIgnoreCase(auth.getName()).orElse(null);
}
}
return direccionService.getForSelectFacturacion(q1, q2, isAdmin ? null : currentUserId);
}
private boolean isOwnerOrAdmin(Authentication auth, Long ownerId) {
if (auth == null) {
return false;

View File

@ -38,6 +38,10 @@ public interface DireccionRepository
// find by user_id
List<Direccion> findByUserId(Long userId);
// find by user_id and direccion_facturacion = true
@Query("SELECT d FROM Direccion d WHERE (:userId IS NULL OR d.user.id = :userId) AND d.direccionFacturacion = true")
List<Direccion> findByUserIdAndDireccionFacturacion(@Param("userId") Long userId);
// find by user_id with deleted
@Query(value = "SELECT * FROM direcciones WHERE user_id = :userId", nativeQuery = true)
List<Direccion> findByUserIdWithDeleted(@Param("userId") Long userId);

View File

@ -77,6 +77,65 @@ public class DireccionService {
}
}
public Map<String, Object> getForSelectFacturacion(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<Direccion> all = repo.findByUserIdAndDireccionFacturacion(userId);
// Mapear a opciones id/text con i18n y filtrar por búsqueda si llega
List<Map<String, String>> 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<String, String> 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<String, Object> resp = new HashMap<>();
resp.put("results", options);
return resp;
} catch (Exception e) {
e.printStackTrace();
return Map.of("results", List.of());
}
}
public Optional<Direccion> findById(Long id) {
return repo.findById(id);
}

View File

@ -1,17 +1,16 @@
checkout.title=Finalizar compra
checkout.summay=Resumen de la compra
checkout.shipping=Envío
checkout.summary=Resumen de la compra
checkout.billing-address=Dirección de facturación
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.onlyOneShipment=Todo el pedido se envía a una única dirección.
checkout.billing-address.title=Seleccione una dirección
checkout.billing-address.new-address=Nueva dirección
checkout.billing-address.select-placeholder=Buscar en direcciones...
checkout.billing-address.errors.noAddressSelected=Debe seleccionar una dirección de facturación para el pedido.
checkout.summary.presupuesto=#Presupuesto
checkout.summary.titulo=Título
checkout.summary.base=Base
checkout.summary.iva-4=IVA 4%
checkout.summary.iva-21=IVA 21%
checkout.summary.envio=Envío
checkout.payment.card=Tarjeta de crédito / débito
checkout.payment.bizum=Bizum
checkout.payment.bank-transfer=Transferencia bancaria
checkout.make-payment=Realizar el pago

View File

@ -8240,7 +8240,8 @@ a {
display: none;
}
.card-radio .form-check-input:checked + .form-check-label {
border-color: #687cfe !important;
border-color: #ff7f5d !important;
background-color: rgba(255, 127, 93, 0.05);
}
.card-radio .form-check-input:checked + .form-check-label:before {
content: "\eb80";
@ -8249,7 +8250,7 @@ a {
top: 2px;
right: 6px;
font-size: 16px;
color: #687cfe;
color: #ff7f5d;
}
.card-radio.dark .form-check-input:checked + .form-check-label:before {
color: #fff;

View File

@ -0,0 +1,5 @@
.direccion-card {
flex: 1 1 350px; /* ancho mínimo 350px, crece si hay espacio */
max-width: 350px; /* opcional, para que no se estiren demasiado */
min-width: 340px; /* protege el ancho mínimo */
}

View File

@ -55,4 +55,5 @@ body {
.form-switch-custom.form-switch-presupuesto .form-check-input:checked::before {
color: #92b2a7;
}
}

View File

@ -42,6 +42,7 @@ $(() => {
if ($('.product').length === 0) {
$("#alert-empty").removeClass("d-none");
$('.cart-content').addClass('d-none');
$('#btn-checkout').prop('disabled', true);
return;
}
else {
@ -59,7 +60,12 @@ $(() => {
return;
}
$(".alert-shipment").addClass("d-none");
$('#btn-checkout').prop('disabled', false);
if ($("#errorEnvio").hasClass("d-none")) {
$('#btn-checkout').prop('disabled', false);
}
else {
$('#btn-checkout').prop('disabled', true);
}
}
else {
const items = $(".product");
@ -92,7 +98,7 @@ $(() => {
item.find(".alert-icon-shipment").addClass("d-none");
}
}
if (errorFound) {
if (errorFound || $("#errorEnvio").hasClass("d-none") === false) {
$(".alert-shipment").removeClass("d-none");
$('#btn-checkout').prop('disabled', true);
}

View File

@ -1,3 +1,5 @@
import { showLoader, hideLoader } from '../loader.js';
$(() => {
const csrfToken = document.querySelector('meta[name="_csrf"]')?.getAttribute('content');
@ -14,8 +16,139 @@ $(() => {
const modalEl = document.getElementById('direccionFormModal');
const modal = bootstrap.Modal.getOrCreateInstance(modalEl);
$('#addBillingAddressBtn').on('click', seleccionarDireccionEnvio);
async function seleccionarDireccionEnvio() {
const { value: direccionId, isDenied } = await Swal.fire({
title: window.languageBundle['checkout.billing-address.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.billing-address.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/facturacion/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.billing-address.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
});
},
preConfirm: () => {
const $select = $('#direccionSelect');
const val = $select.val();
if (!val) {
Swal.showValidationMessage(
window.languageBundle['checkout.billing-address.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
showLoader();
let uri = `/checkout/get-address/${direccionId}`;
const response = await fetch(uri);
if (response.ok) {
const html = await response.text();
$('#direccion-div').append(html);
$('#addBillingAddressBtn').addClass('d-none');
hideLoader();
return true;
}
hideLoader();
return false;
}
hideLoader();
return false;
}
$(document).on('click', '.btn-delete-direccion', function (e) {
e.preventDefault();
const $card = $(this).closest('.direccion-card');
const $div = $card.parent();
$card.remove();
$('#addBillingAddressBtn').removeClass('d-none');
});
});

View File

@ -24,7 +24,7 @@
<td><span th:text="#{cart.resumen.iva-21}"></span> : </td>
<td class="text-end" id="iva-21-cesta" th:text="${summary.iva21}"></td>
</tr>
<tr id="tr-iva-21">
<tr>
<td><span
th:text="#{cart.resumen.descuento} + ' (' + ${summary.fidelizacion} + ')'"></span>
: </td>
@ -38,12 +38,8 @@
</tr>
</tbody>
</table>
<form th:action="@{/pagos/redsys/crear}" method="post">
<input type="hidden" name="order" value="123456789012" />
<input type="hidden" name="amountCents" value="12525" />
<button id="btn-checkout" type="submit" class="btn btn-secondary w-100 mt-2"
th:text="#{cart.resumen.tramitar}">Checkout</button>
</form>
<button id="btn-checkout" onclick="location.href='/checkout'" class="btn btn-secondary w-100 mt-2"
th:text="#{cart.resumen.tramitar}">Checkout</button>
</div>
<!-- end table-responsive -->
</div>

View File

@ -1,43 +0,0 @@
<div>
<div
th:replace="imprimelibros/partials/modal-form :: modal('direccionFormModal', 'direcciones.add', 'modal-md', 'direccionFormModalBody')">
</div>
<div class="card ribbon-box border shadow-none mb-lg-0 material-shadow">
<div class="card-body">
<div class="ribbon ribbon-primary ribbon-shape" th:text="#{checkout.shipping.order}">Envio del pedido
</div>
</div>
<div class="ribbon-content mt-4">
<div class="px-2 mb-2">
<p th:text="#{checkout.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="onlyOneShipment" checked />
<label for="onlyOneShipment" class="form-label d-flex align-items-center mb-0">
<span th:text="#{checkout.shipping.onlyOneShipment}" class="me-2"></span>
</label>
</div>
<button type="button" class="btn btn-secondary" id="addOrderAddress"
th:text="#{checkout.shipping.add}">Añadir dirección</button>
<div id="orderShippingAddressesContainer" class="mt-4"></div>
<div id="orderShippingMultipleAddressesContainer d-none" class="mt-4"></div>
</div>
</div>
</div>
<div class="card ribbon-box border shadow-none mb-lg-0 material-shadow mt-4" th:if="${hasSample}">
<div class="card-body">
<div class="ribbon ribbon-primary ribbon-shape" th:text="#{checkout.shipping.samples}">Envio de pruebas
</div>
</div>
<div class="ribbon-content mt-4">
</div>
</div>
<!-- End Ribbon Shape -->
</div>

View File

@ -1,3 +1,36 @@
<div>
<h5 class="mb-3" th:text="#{checkout.payment}">Método de pago</h5>
<div class="row g-4">
<div class="col-lg-4 col-sm-6">
<div class="form-check card-radio">
<input id="paymentMethod01" name="paymentMethod" type="radio" class="form-check-input" checked>
<label class="form-check-label" for="paymentMethod01">
<span class="fs-16 text-muted me-2"><i class="mdi mdi-credit-card-outline align-bottom"></i></span>
<span class="fs-14 text-wrap" th:text="#{checkout.payment.card}"></span>
</label>
</div>
</div>
<div class="col-lg-4 col-sm-6">
<div class="form-check card-radio">
<input id="paymentMethod02" name="paymentMethod" type="radio" class="form-check-input">
<label class="form-check-label" for="paymentMethod02">
<span class="fs-16 text-muted me-2"><i class="mdi mdi-wallet-outline align-bottom"></i></span>
<span class="fs-14 text-wrap" th:text="#{checkout.payment.bizum}"></span>
</label>
</div>
</div>
<div class="col-lg-4 col-sm-6">
<div class="form-check card-radio">
<input id="paymentMethod03" name="paymentMethod" type="radio" class="form-check-input">
<label class="form-check-label" for="paymentMethod03">
<span class="fs-16 text-muted me-2"><i class="mdi mdi-bank-transfer align-bottom"></i></span>
<span class="fs-14 text-wrap" th:text="#{checkout.payment.bank-transfer}"></span>
</label>
</div>
</div>
</div>
</div>

View File

@ -0,0 +1,51 @@
<div th:fragment="checkoutSummary(summary)" class="col-xl-4 cart-summary-container">
<div class="sticky-side-div">
<div class="card">
<div class="card-header border-bottom-dashed">
<h5 th:text="#{checkout.summary}" class="card-title mb-0"></h5>
</div>
<div class="card-body pt-2">
<div class="table-responsive">
<table class="table table-borderless mb-0">
<tbody>
<tr>
<td><span th:text="#{cart.resumen.base}"></span></td>
<td class="text-end" id="base-cesta" th:text="${summary.base}"></td>
</tr>
<tr>
<td><span th:text="#{cart.resumen.envio}"></span></td>
<td class="text-end" id="envio-cesta" th:text="${summary.shipment}"></td>
</tr>
<tr id="tr-iva-4">
<td><span th:text="#{cart.resumen.iva-4}"></span> : </td>
<td class="text-end" id="iva-4-cesta" th:text="${summary.iva4}"></td>
</tr>
<tr id="tr-iva-21">
<td><span th:text="#{cart.resumen.iva-21}"></span> : </td>
<td class="text-end" id="iva-21-cesta" th:text="${summary.iva21}"></td>
</tr>
<tr>
<td><span
th:text="#{cart.resumen.descuento} + ' (' + ${summary.fidelizacion} + ')'"></span>
: </td>
<td class="text-end" id="descuento-cesta" th:text="${summary.descuento}"></td>
</tr>
<tr class="table-active">
<th><span th:text="#{cart.resumen.total}"></span>:</th>
<td class="text-end">
<span id="total-cesta" class="fw-semibold" th:text="${summary.total}"></span>
</td>
</tr>
</tbody>
</table>
<button id="btn-checkout" onclick="location.href='/checkout'" class="btn btn-secondary w-100 mt-2"
th:text="#{checkout.make-payment}" disabled>Checkout</button>
</div>
<!-- end table-responsive -->
</div>
</div>
</div>
<!-- end stickey -->
</div>

View File

@ -9,7 +9,7 @@
<link th:href="@{/assets/libs/datatables/dataTables.bootstrap5.min.css}" rel="stylesheet" />
</th:block>
<th:block layout:fragment="pagecss">
<link th:href="@{/assets/css/presupuestador.css}" rel="stylesheet" />
<link th:href="@{/assets/css/checkout.css}" rel="stylesheet" />
</th:block>
</head>
@ -22,6 +22,10 @@
<th:block layout:fragment="content">
<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">
<nav aria-label="breadcrumb">
<ol class="breadcrumb">
@ -32,144 +36,52 @@
</nav>
</div>
<div id="sectionLoader" class="position-absolute top-0 start-0 w-100 h-100 d-none justify-content-center align-items-center
bg-body bg-opacity-75" style="z-index:10;">
<div class="spinner-border" role="status" style="width:2.5rem;height:2.5rem;">
<span class="visually-hidden">Cargando…</span>
</div>
</div>
<div class="row">
<div class="col-xl-8 col-12">
<div class="card">
<div class="card-body">
<div class="step-arrow-nav mt-n3 mx-n3 mb-3">
<div>
<h5 th:text="#{checkout.billing-address}" class="mb-3">Dirección de envío</h5>
<ul class="nav nav-pills nav-justified custom-nav" role="tablist">
<li class="nav-item" role="presentation">
<button class="nav-link fs-15 p-3 active" id="pills-shipping-tab"
data-bs-target="#pills-shipping" type="button" role="tab"
aria-controls="pills-shipping" aria-selected="true">
<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="#{checkout.shipping}">Envío</label>
</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link fs-15 p-3" id="pills-payment-tab"
data-bs-target="#pills-payment" type="button" role="tab"
aria-controls="pills-payment" aria-selected="false">
<i
class="ri-money-euro-box-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="#{checkout.payment}">Método de
pago</label>
</button>
</li>
</ul>
<button type="button" class="btn btn-secondary mb-3" id="addBillingAddressBtn"
th:text="#{cart.shipping.add}">Añadir dirección
</button>
<div id="direccion-div">
</div>
</div>
<div class="tab-content">
<div class="tab-pane fade show active" id="pills-shipping" role="tabpanel"
aria-labelledby="pills-shipping-tab">
<div th:include="~{imprimelibros/checkout/_envio.html}">
</div>
</div>
<!-- end tab pane -->
<div class="tab-pane fade" id="pills-payment" role="tabpanel"
aria-labelledby="pills-payment-tab">
<div th:include="~{imprimelibros/checkout/_pago.html}">
</div>
</div>
<!-- end tab pane -->
</div>
</div>
<div class="card">
<div class="card-body">
<div th:include="~{imprimelibros/checkout/_pago.html}">
</div>
</div>
</div>
</div>
<div class="col-xl-4">
<div class="sticky-side-div">
<div class="card">
<div class="card-header border-bottom-dashed">
<h5 th:text="#{checkout.summay}" class="card-title mb-1"></h5>
</div>
<div class="card-body pt-2">
<div class="table-responsive table-card">
<table class="table table-borderless align-middle mb-0">
<thead class="table-light text-muted">
<tr>
<th style="width: 90px;" scope="col"
th:text="#{checkout.summary.presupuesto}">Presupuesto</th>
<th scope="col" th:text="#{checkout.summary.titulo}">Título</th>
<th scope="col" class="text-end" th:text="#{checkout.summary.base}">
Base</th>
<th class="d-none"></th>
</tr>
</thead>
<tbody>
<tr th:each="item : ${items}">
<td>
<span th:text="${item.presupuestoId}">PRESUPUESTO-001</span>
</td>
<td>
<span th:text="${item.titulo}">Título del presupuesto</span>
</td>
<td class="text-end">
<span th:text="${item.baseTotal}">
0,00</span>
</td>
<td class="d-none">
<span th:text="${item.tirada}"></span>
</td>
</tr>
</tbody>
<tfoot>
<tr>
<td colspan="2"><span th:text="#{cart.resumen.base}"></span></td>
<td class="text-end" id="base-cesta"></td>
</tr>
<tr id="tr-iva-4">
<td colspan="2"><span th:text="#{cart.resumen.iva-4}"></span> : </td>
<td class="text-end" id="iva-4-cesta"></td>
</tr>
<tr id="tr-iva-21">
<td colspan="2"><span th:text="#{cart.resumen.iva-21}"></span> : </td>
<td class="text-end" id="iva-21-cesta"></td>
</tr>
<tr class="table-active">
<td colspan="2"><span th:text="#{cart.resumen.total}"></span>:</td>
<td class="text-end">
<span id="total-cesta" class="fw-semibold">
</span>
</td>
</tr>
</tfoot>
</table>
<button type="button" class="btn btn-secondary w-100 mt-2"
th:text="#{cart.resumen.tramitar}">Checkout</button>
</div>
<!-- end table-responsive -->
</div>
</div>
<div th:replace="~{imprimelibros/checkout/_summary :: checkoutSummary(${summary})}"></div>
<div class="alert border-dashed alert-danger" role="alert">
<div class="d-flex align-items-center">
<div class="ms-2">
<h5 class="fs-14 text-danger fw-semibold"
th:text="#{cart.resumen.fidelizacion}"></h5>
</div>
</div>
</div>
</div>
<!-- end stickey -->
</div>
</div>
</div>
</div>
</th:block>
</th:block>
<th:block th:replace="~{theme/partials/vendor-scripts :: scripts}" />
<th:block layout:fragment="pagejs">
<script th:inline="javascript">
window.languageBundle = /*[[${languageBundle}]]*/ {};
</script>
<script type="module" th:src="@{/assets/js/pages/imprimelibros/checkout/checkout.js}"></script>
</th:block>
<th:block th:replace="~{theme/partials/vendor-scripts :: scripts}" />
<th:block layout:fragment="pagejs">
<script th:inline="javascript">
window.languageBundle = /*[[${languageBundle}]]*/ {};
</script>
<script type="module" th:src="@{/assets/js/pages/imprimelibros/checkout/checkout.js}"></script>
</th:block>
</body>
</html>

View File

@ -0,0 +1,31 @@
<div th:fragment="direccionBillingCard(direccion, pais)" name="direccion"
class="card card border mb-3 direccion-card bg-light w-100 mx-2">
<div class="card-body">
<input type="hidden" class="direccion-id" th:value="${direccion.id}" />
<input type="hidden" class="direccion-cp" th:value="${direccion.cp}" />
<input type="hidden" class="direccion-pais-code3" th:value="${direccion.pais.code3}" />
<div class="row g-3 align-items-start flex-nowrap mb-2">
<div class="col">
<span class="mb-2 fw-semibold d-block text-muted text-uppercase text-break" th:text="${direccion.alias}"></span>
<span class="fs-14 mb-1 d-block text-break" th:text="${direccion.razonSocial}"></span>
<span class="fs-14 mb-1 d-block text-break" th:text="${direccion.tipoIdentificacionFiscal + ': ' + direccion.identificacionFiscal}"></span>
<span class="text-muted fw-normal text-wrap mb-1 d-block text-break" th:text="${direccion.direccion + ', ' + direccion.ciudad}"></span>
<span class="text-muted fw-normal d-block text-break" th:text="${direccion.cp + ', ' + direccion.provincia}"></span>
<span class="text-muted fw-normal d-block text-break" th:text="${pais}"></span>
<span class="text-muted fw-normal d-block text-break"
th:text="#{'direcciones.telefono'} + ': ' + ${direccion.telefono}"></span>
</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">
<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>
<span th:text="#{'direcciones.btn.delete'}">Eliminar</span>
</a>
</div>
</div>
</div>