terminado. trabajando en el carrito. falta mensaje de ya añadido

This commit is contained in:
2025-10-17 13:31:09 +02:00
parent 46715d1017
commit 06e03afa04
21 changed files with 251 additions and 113 deletions

View File

@ -94,7 +94,8 @@ public class CartController {
} }
/** Eliminar línea por presupuesto_id (opcional) */ /** Eliminar línea por presupuesto_id (opcional) */
@DeleteMapping("/remove/presupuesto/{presupuestoId}") @DeleteMapping("/delete/item/{presupuestoId}")
@ResponseBody
public String removeByPresupuesto(@PathVariable Long presupuestoId, Principal principal) { public String removeByPresupuesto(@PathVariable Long presupuestoId, Principal principal) {
service.removeByPresupuesto(currentUserId(principal), presupuestoId); service.removeByPresupuesto(currentUserId(principal), presupuestoId);
return "redirect:/cart"; return "redirect:/cart";

View File

@ -19,6 +19,7 @@ import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectMapper;
import com.imprimelibros.erp.presupuesto.classes.PresupuestoFormatter; import com.imprimelibros.erp.presupuesto.classes.PresupuestoFormatter;
import com.imprimelibros.erp.presupuesto.dto.Presupuesto; import com.imprimelibros.erp.presupuesto.dto.Presupuesto;
import com.imprimelibros.erp.common.Utils;
import com.imprimelibros.erp.presupuesto.PresupuestoRepository; import com.imprimelibros.erp.presupuesto.PresupuestoRepository;
@ -30,15 +31,17 @@ public class CartService {
private final MessageSource messageSource; private final MessageSource messageSource;
private final PresupuestoFormatter presupuestoFormatter; private final PresupuestoFormatter presupuestoFormatter;
private final PresupuestoRepository presupuestoRepo; private final PresupuestoRepository presupuestoRepo;
private final Utils utils;
public CartService(CartRepository cartRepo, CartItemRepository itemRepo, public CartService(CartRepository cartRepo, CartItemRepository itemRepo,
MessageSource messageSource, PresupuestoFormatter presupuestoFormatter, MessageSource messageSource, PresupuestoFormatter presupuestoFormatter,
PresupuestoRepository presupuestoRepo) { PresupuestoRepository presupuestoRepo, Utils utils) {
this.cartRepo = cartRepo; this.cartRepo = cartRepo;
this.itemRepo = itemRepo; this.itemRepo = itemRepo;
this.messageSource = messageSource; this.messageSource = messageSource;
this.presupuestoFormatter = presupuestoFormatter; this.presupuestoFormatter = presupuestoFormatter;
this.presupuestoRepo = presupuestoRepo; this.presupuestoRepo = presupuestoRepo;
this.utils = utils;
} }
/** Devuelve el carrito activo o lo crea si no existe. */ /** Devuelve el carrito activo o lo crea si no existe. */
@ -137,65 +140,14 @@ public class CartService {
resumen.put("presupuestoId", presupuesto.getId()); resumen.put("presupuestoId", presupuesto.getId());
ObjectMapper mapper = new ObjectMapper(); Map<String, Object> detalles = utils.getTextoPresupuesto(presupuesto, locale);
List<Map<String, Object>> servicios = new ArrayList<>();
if (presupuesto.getServiciosJson() != null && !presupuesto.getServiciosJson().isBlank())
try{
servicios = mapper.readValue(presupuesto.getServiciosJson(), new TypeReference<>() {
});
} catch (JsonProcessingException e) {
// Manejar la excepción
}
boolean hayDepositoLegal = servicios != null && servicios.stream() resumen.put("baseTotal", Utils.formatCurrency(presupuesto.getBaseImponible(), locale));
.map(m -> java.util.Objects.toString(m.get("id"), "")) resumen.put("base", presupuesto.getBaseImponible());
.map(String::trim) resumen.put("iva4", presupuesto.getIvaImporte4());
.anyMatch("deposito-legal"::equals); resumen.put("iva21", presupuesto.getIvaImporte21());
List<HashMap<String, Object>> lineas = new ArrayList<>(); resumen.put("resumen", detalles);
HashMap<String, Object> linea = new HashMap<>();
Double precio_unitario = 0.0;
Double precio_total = 0.0;
BigDecimal total = BigDecimal.ZERO;
linea.put("descripcion", presupuestoFormatter.resumen(presupuesto, servicios, locale));
linea.put("cantidad", presupuesto.getSelectedTirada() != null ? presupuesto.getSelectedTirada() : 0);
precio_unitario = (presupuesto.getPrecioUnitario() != null ? presupuesto.getPrecioUnitario().doubleValue() : 0.0);
precio_total = (presupuesto.getPrecioTotalTirada() != null ? presupuesto.getPrecioTotalTirada().doubleValue() : 0.0);
linea.put("precio_unitario", precio_unitario);
linea.put("precio_total", BigDecimal.valueOf(precio_total).setScale(2, RoundingMode.HALF_UP));
total = total.add(BigDecimal.valueOf(precio_total));
lineas.add(linea);
if (hayDepositoLegal) {
linea = new HashMap<>();
linea.put("descripcion", messageSource.getMessage("presupuesto.resumen-deposito-legal", null, locale));
linea.put("cantidad", 4);
linea.put("precio_unitario", precio_unitario);
linea.put("precio_total", BigDecimal.valueOf(precio_unitario * 4).setScale(2, RoundingMode.HALF_UP));
total = total.add(BigDecimal.valueOf(precio_unitario * 4));
lineas.add(linea);
}
List<Map<String, Object>> serviciosExtras = new ArrayList<>();
if (servicios != null) {
for (Map<String, Object> servicio : servicios) {
HashMap<String, Object> servicioData = new HashMap<>();
servicioData.put("id", servicio.get("id"));
servicioData.put("descripcion", servicio.get("label"));
servicioData.put("precio", servicio.get("id").equals("marcapaginas")
? Double.parseDouble(servicio.get("price").toString())
/ Double.parseDouble(servicio.get("units").toString())
: servicio.get("price"));
total = total.add(BigDecimal.valueOf(Double.parseDouble(servicioData.get("precio").toString())));
servicioData.put("unidades", servicio.get("units"));
serviciosExtras.add(servicioData);
}
}
NumberFormat currencyFormat = NumberFormat.getCurrencyInstance(locale);
String formattedString = currencyFormat.format(total.setScale(2, RoundingMode.HALF_UP).doubleValue());
resumen.put("total", formattedString);
resumen.put("lineas", lineas);
resumen.put("servicios", serviciosExtras);
return resumen; return resumen;
} }

View File

@ -93,18 +93,28 @@ public class Utils {
if (hayDepositoLegal) { if (hayDepositoLegal) {
linea = new HashMap<>(); linea = new HashMap<>();
linea.put("descripcion", linea.put("descripcion",
messageSource.getMessage("pdf.ejemplares-deposito-legal", new Object[]{4}, locale)); messageSource.getMessage("pdf.ejemplares-deposito-legal", new Object[] { 4 },
locale));
lineas.add(linea); lineas.add(linea);
} }
String serviciosExtras = ""; String serviciosExtras = "";
if (servicios != null) { if (servicios != null) {
for (Map<String, Object> servicio : servicios) { for (Map<String, Object> servicio : servicios) {
serviciosExtras += messageSource.getMessage( if ("deposito-legal".equals(servicio.get("id")) ||
"presupuesto.extras-" + servicio.get("id"), null, locale) + ", "; "service-isbn".equals(servicio.get("id"))) {
serviciosExtras += messageSource.getMessage(
"presupuesto.extras-" + servicio.get("id"), null, locale)
+ ", ";
} else {
serviciosExtras += messageSource.getMessage(
"presupuesto.extras-" + servicio.get("id"), null, locale)
.toLowerCase() + ", ";
}
} }
if (!serviciosExtras.isEmpty()) { if (!serviciosExtras.isEmpty()) {
serviciosExtras = serviciosExtras.substring(0, serviciosExtras.length() - 2);; serviciosExtras = serviciosExtras.substring(0, serviciosExtras.length() - 2);
;
} }
if (servicios.stream().anyMatch(service -> "marcapaginas".equals(service.get("id")))) { if (servicios.stream().anyMatch(service -> "marcapaginas".equals(service.get("id")))) {
ObjectMapper mapperServicio = new ObjectMapper(); ObjectMapper mapperServicio = new ObjectMapper();

View File

@ -113,8 +113,12 @@ public class SecurityConfig {
RequestMatcher notStatic = new AndRequestMatcher( RequestMatcher notStatic = new AndRequestMatcher(
new NegatedRequestMatcher(PathRequest.toStaticResources().atCommonLocations()), new NegatedRequestMatcher(PathRequest.toStaticResources().atCommonLocations()),
new NegatedRequestMatcher(pathStartsWith("/assets/"))); new NegatedRequestMatcher(pathStartsWith("/assets/")));
RequestMatcher cartCount = new AndRequestMatcher(
new NegatedRequestMatcher(PathRequest.toStaticResources().atCommonLocations()),
new NegatedRequestMatcher(pathStartsWith("/cart/count")));
cache.setRequestMatcher(new AndRequestMatcher(htmlPage, nonAjax, notStatic, notWellKnown)); cache.setRequestMatcher(new AndRequestMatcher(htmlPage, nonAjax, notStatic, notWellKnown, cartCount));
rc.requestCache(cache); rc.requestCache(cache);
}) })
// ======================================================== // ========================================================
@ -135,7 +139,8 @@ public class SecurityConfig {
"/presupuesto/public/**", "/presupuesto/public/**",
"/error", "/error",
"/favicon.ico", "/favicon.ico",
"/.well-known/**" // opcional "/.well-known/**", // opcional
"/api/pdf/presupuesto/**"
).permitAll() ).permitAll()
.requestMatchers("/users/**").hasAnyRole("SUPERADMIN", "ADMIN") .requestMatchers("/users/**").hasAnyRole("SUPERADMIN", "ADMIN")
.anyRequest().authenticated()) .anyRequest().authenticated())

View File

@ -118,7 +118,7 @@ public class PdfService {
model.put("empresa", empresa); model.put("empresa", empresa);
model.put("cliente", Map.of( model.put("cliente", Map.of(
"nombre", presupuesto.getUser().getFullName())); "nombre", presupuesto.getUser() != null ? presupuesto.getUser().getFullName() : ""));
model.put("titulo", presupuesto.getTitulo()); model.put("titulo", presupuesto.getTitulo());

View File

@ -38,8 +38,8 @@ public class PresupuestoFormatter {
Object[] args = { Object[] args = {
p.getSelectedTirada(), p.getSelectedTirada(),
encuadernacion, encuadernacion.toLowerCase(),
tipoImpresion, tipoImpresion.toLowerCase(),
(p.getPaginasColorTotal() != null ? p.getPaginasColorTotal() : p.getPaginasColor()) (p.getPaginasColorTotal() != null ? p.getPaginasColorTotal() : p.getPaginasColor())
+ p.getPaginasNegro(), + p.getPaginasNegro(),
p.getAncho(), p.getAlto(), p.getAncho(), p.getAlto(),

View File

@ -3,3 +3,12 @@ cart.empty=Tu cesta de la compra está vacía.
cart.item.presupuesto-numero=Presupuesto # cart.item.presupuesto-numero=Presupuesto #
cart.precio=Precio cart.precio=Precio
cart.resumen.title=Resumen de la cesta
cart.resumen.base=Base imponible:
cart.resumen.iva-4=IVA 4%:
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.

View File

@ -14,8 +14,6 @@ pdf.presupuesto.client=CLIENTE:
pdf.presupuesto.date=FECHA: pdf.presupuesto.date=FECHA:
pdf.presupuesto.titulo=Título: pdf.presupuesto.titulo=Título:
pdf.presupuesto.descripcion=Descripción:
pdf.table.tirada=TIRADA pdf.table.tirada=TIRADA
pdf.table.impresion=IMPRESIÓN pdf.table.impresion=IMPRESIÓN
pdf.table.servicios=SERVICIOS pdf.table.servicios=SERVICIOS
@ -23,11 +21,13 @@ pdf.table.iva-4=IVA 4%
pdf.table.iva-21=IVA 21% pdf.table.iva-21=IVA 21%
pdf.table.precio-total=PRECIO TOTAL pdf.table.precio-total=PRECIO TOTAL
pdf.servicios-adicionales=<u>Servicios adicionales:</u> pdf.servicios-adicionales=Servicios adicionales:
pdf.ejemplares-deposito-legal=Impresión de {0} ejemplares de depósito legal<br /> pdf.ejemplares-deposito-legal=Impresión de {0} ejemplares de depósito legal<br />
pdf.datos-maquetacion=Datos de maquetación: pdf.datos-maquetacion=Datos de maquetación:
pdf.datos-marcapaginas=Datos de marcapáginas: pdf.datos-marcapaginas=Datos de marcapáginas:
pdf.incluye-envio=El presupuesto incluye el envío a una dirección de la península.
pdf.politica-privacidad=Política de privacidad pdf.politica-privacidad=Política de privacidad
pdf.politica-privacidad.responsable=Responsable: Impresión Imprime Libros - CIF: B04998886 - Teléfono de contacto: 910052574 pdf.politica-privacidad.responsable=Responsable: Impresión Imprime Libros - CIF: B04998886 - Teléfono de contacto: 910052574
pdf.politica-privacidad.correo-direccion=Correo electrónico: info@imprimelibros.com - Dirección postal: Calle José Picón, Nº 28 Local A, 28028, Madrid pdf.politica-privacidad.correo-direccion=Correo electrónico: info@imprimelibros.com - Dirección postal: Calle José Picón, Nº 28 Local A, 28028, Madrid

View File

@ -213,17 +213,17 @@ presupuesto.resumen.tabla.base=Base
presupuesto.resumen.tabla.iva4=I.V.A. (4%) presupuesto.resumen.tabla.iva4=I.V.A. (4%)
presupuesto.resumen.tabla.iva21=I.V.A. (21%) presupuesto.resumen.tabla.iva21=I.V.A. (21%)
presupuesto.resumen.tabla.total=Total presupuesto presupuesto.resumen.tabla.total=Total presupuesto
presupuesto.resumen-texto=Impresion de {0} unidades encuadernadas en {1} en {2} con {3} páginas en formato {4} x {5} mm. \ presupuesto.resumen-texto=Impresion de {0} ejemplares con {3} páginas, tamaño {4}x{5} mm e impresas en {2} con encuadernación {1}. \
<ul> \ <ul> \
<li>Papel interior {6} {7} gr.</li> \ <li>Papel interior {6} {7} gr.</li> \
<li>Cubierta {8} en {9} {10} gr.</li> <li>Cubierta {8} en {9} {10} gr.</li>
presupuesto.resumen-texto-impresion-caras-cubierta=<li>Impresa a {0}.</li> presupuesto.resumen-texto-impresion-caras-cubierta=<li>Impresa a {0}.</li>
presupuesto.resumen-texto-solapas-cubierta=<li>Solapas de {0} mm.</li> presupuesto.resumen-texto-solapas-cubierta=<li>Solapas de {0} mm.</li>
presupuesto.resumen-texto-guardas-cabezada= <li>Guardas {0} en {1} {2}. Color de la cabezada: {3}.</li> presupuesto.resumen-texto-guardas-cabezada= <li>Guardas {0} en {1} {2}. Color de la cabezada: {3}.</li>
presupuesto.resumen-texto-acabado-cubierta= <li>Acabado {0}. </li> presupuesto.resumen-texto-acabado-cubierta= <li>Acabado: {0}. </li>
presupuesto.resumen-texto-end=</ul> presupuesto.resumen-texto-end=</ul>
presupuesto.resumen-texto-sobrecubierta=<li>Sobrecubierta impresa en {0} {1} gr. <ul><li>Acabado {2}</li><li>Solapas: {3} mm.</li></ul></li> presupuesto.resumen-texto-sobrecubierta=<li>Sobrecubierta impresa en {0} {1} gr. <ul><li>Acabado: {2}</li><li>Solapas: {3} mm.</li></ul></li>
presupuesto.resumen-texto-faja=<li>Faja impresa en {0} {1} gr. con un alto de {2} mm. <ul><li>Acabado {3}</li><li>Solapas: {4} mm.</li></ul></li> presupuesto.resumen-texto-faja=<li>Faja impresa en {0} {1} gr. con un alto de {2} mm. <ul><li>Acabado: {3}</li><li>Solapas: {4} mm.</li></ul></li>
presupuesto.resumen-deposito-legal=Ejemplares para el Depósito Legal presupuesto.resumen-deposito-legal=Ejemplares para el Depósito Legal
presupuesto.volver-extras=Extras del libro presupuesto.volver-extras=Extras del libro
presupuesto.resumen.inicie-sesion=Inicie sesión para continuar presupuesto.resumen.inicie-sesion=Inicie sesión para continuar

View File

@ -0,0 +1,67 @@
import { formateaMoneda } from '../../imprimelibros/utils.js';
$(() => {
updateTotal();
function updateTotal() {
const items = $(".product");
let iva4 = 0;
let iva21 = 0;
let base = 0;
for (let i = 0; i < items.length; i++) {
const item = $(items[i]);
const b = item.data("base");
const i4 = item.data("iva-4");
const i21 = item.data("iva-21");
base += parseFloat(b) || 0;
iva4 += parseFloat(i4) || 0;
iva21 += parseFloat(i21) || 0;
}
$("#base-cesta").text(formateaMoneda(base));
if (iva4 > 0) {
$("#iva-4-cesta").text(formateaMoneda(iva4));
$("#tr-iva-4").show();
} else {
$("#tr-iva-4").hide();
}
if (iva21 > 0) {
$("#iva-21-cesta").text(formateaMoneda(iva21));
$("#tr-iva-21").show();
} else {
$("#tr-iva-21").hide();
}
const total = base + iva4 + iva21;
$("#total-cesta").text(formateaMoneda(total));
}
$(document).on("click", ".delete-item", async function (event) {
event.preventDefault();
const cartItemId = $(this).data("cart-item-id");
const card = $(this).closest('.card.product');
// CSRF (Spring Security)
const csrfToken = document.querySelector('meta[name="_csrf"]')?.content || '';
const csrfHeader = document.querySelector('meta[name="_csrf_header"]')?.content || 'X-CSRF-TOKEN';
try {
const res = await fetch(`/cart/delete/item/${cartItemId}`, {
method: 'DELETE',
headers: { [csrfHeader]: csrfToken }
});
if (!res.ok) {
console.error('Error al eliminar. Status:', res.status);
return;
}
else{
card?.remove();
updateTotal();
}
} catch (err) {
console.error('Error en la solicitud:', err);
}
});
});

View File

@ -1700,6 +1700,7 @@ export default class PresupuestoWizard {
window.PRESUPUESTO_ID = data.presupuesto_id; window.PRESUPUESTO_ID = data.presupuesto_id;
} }
body.presupuesto.id = window.PRESUPUESTO_ID || body.presupuesto.id || null; body.presupuesto.id = window.PRESUPUESTO_ID || body.presupuesto.id || null;
this.opts.presupuestoId = window.PRESUPUESTO_ID;
this.#updateResumenTable(data); this.#updateResumenTable(data);
}).catch((error) => { }).catch((error) => {

View File

@ -1,5 +1,7 @@
<!-- _cartItem.html --> <!-- _cartItem.html -->
<div th:fragment="cartItem(item)" class="card product mb-3 shadow-sm"> <div th:fragment="cartItem(item)" class="card product mb-3 shadow-sm" th:attr="data-iva-4=${item.iva4},
data-iva-21=${item.iva21},
data-base=${item.base}">
<div class="card-body"> <div class="card-body">
<div class="row gy-3"> <div class="row gy-3">
@ -25,20 +27,44 @@
</h5> </h5>
<!-- Detalles opcionales (ej: cliente, fecha, etc.) --> <!-- Detalles opcionales (ej: cliente, fecha, etc.) -->
<ul class="list-inline text-muted mb-2"> <ul class="list-inline text-muted mb-1">
<div th:each="linea : ${item.lineas}"> <div th:each="linea : ${item.resumen.lineas}">
<li class="list-inline-item me-3"> <li class="list-inline-item me-3">
<div th:utext="${linea['descripcion']}"></div> <div th:utext="${linea['descripcion']}"></div>
</li> </li>
</div> </div>
</ul> </ul>
<ul class="list-inline text-muted mb-1" th:if="${item.resumen.servicios != null}">
<span th:utext="#{pdf.servicios-adicionales}">Servicios adicionales</span>
<span class="spec-label" th:text="${item.resumen.servicios}"></span>
</ul>
<ul class="list-inline text-muted mb-1" th:if="${item.resumen != null
and #maps.containsKey(item.resumen, 'datosMaquetacion')
and item.resumen['datosMaquetacion'] != null}">
<li class="list-inline-item spec-row mb-1">
<span th:text="#{pdf.datos-maquetacion}">Datos de maquetación:</span>
<span th:utext="${item.resumen.datosMaquetacion}"></span>
</li>
</ul>
<ul class="list-inline text-muted mb-1" th:if="${item.resumen != null
and #maps.containsKey(item.resumen, 'datosMarcapaginas')
and item.resumen['datosMarcapaginas'] != null}">
<li class="list-inline-item spec-row mb-1">
<span th:text="#{pdf.datos-marcapaginas}">Datos de marcapáginas:</span>
<span th:utext="${item.resumen.datosMarcapaginas}"></span>
</li>
</ul>
</div> </div>
<!-- Precio o totales (si los tienes) --> <!-- Precio o totales (si los tienes) -->
<div class="col-sm-auto text-end"> <div class="col-sm-auto text-end">
<p class="text-muted mb-1" th:text="#{cart.precio}">Precio</p> <p class="text-muted mb-1" th:text="#{cart.precio}">Precio</p>
<h5 class="fs-14 mb-0"> <h5 class="fs-14 mb-0">
<span th:text="${item.total != null ? item.total : '-'}">0,00</span> <span th:text="${item.baseTotal != null ? item.baseTotal : '-'}">0,00</span>
</h5> </h5>
</div> </div>
</div> </div>
@ -50,14 +76,10 @@
<div class="d-flex flex-wrap my-n1"> <div class="d-flex flex-wrap my-n1">
<!-- Botón eliminar --> <!-- Botón eliminar -->
<div> <div>
<form th:action="@{|/cart/${item.cartItemId}/remove|}" method="post" class="d-inline"> <a href="javascript:void(0);" class="d-block text-body p-1 px-2 delete-item"
<input type="hidden" name="_method" value="delete" /> th:attr="data-cart-item-id=${item.cartItemId}">
<input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}" /> <i class="ri-delete-bin-fill text-muted align-bottom me-1"></i> Eliminar
<a href="#" class="d-block text-body p-1 px-2" </a>
onclick="this.closest('form').submit(); return false;">
<i class="ri-delete-bin-fill text-muted align-bottom me-1"></i> Eliminar
</a>
</form>
</div> </div>
</div> </div>
</div> </div>

View File

@ -32,18 +32,67 @@
</nav> </nav>
</div> </div>
<div class="container-fluid row gy-4">
<div class="container-fluid">
<div th:if="${items.isEmpty()}"> <div th:if="${items.isEmpty()}">
<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>
<div th:unless="${#lists.isEmpty(items)}"> <div class="col-xl-8 col-12">
<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> </div>
<div class="col-xl-4">
<div class="sticky-side-div">
<div class="card">
<div class="card-header border-bottom-dashed">
<h5 th:text="#{cart.resumen.title}" 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"></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"></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"></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">
</span>
</td>
</tr>
</tbody>
</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 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>
</th:block> </th:block>
<th:block th:replace="~{theme/partials/vendor-scripts :: scripts}" /> <th:block th:replace="~{theme/partials/vendor-scripts :: scripts}" />
@ -51,6 +100,8 @@
<script th:inline="javascript"> <script th:inline="javascript">
window.languageBundle = /*[[${languageBundle}]]*/ {}; window.languageBundle = /*[[${languageBundle}]]*/ {};
</script> </script>
<script type="module" th:src="@{/assets/js/pages/imprimelibros/cart/cart.js}"></script>
</th:block> </th:block>
</body> </body>

View File

@ -54,9 +54,12 @@
<tr> <tr>
<td class="text-start"><span th:text="#{'pdf.presupuesto.number'}" class="lbl">PRESUPUESTO Nº:</span> <span <td class="text-start"><span th:text="#{'pdf.presupuesto.number'}" class="lbl">PRESUPUESTO Nº:</span> <span
class="val" th:text="${numero}">153153</span></td> class="val" th:text="${numero}">153153</span></td>
<td class="text-center"><span th:text="#{pdf.presupuesto.client}" class="lbl">CLIENTE:</span> <span class="val" <td class="text-center">
<span th:if="${cliente?.nombre} != ''" th:text="#{pdf.presupuesto.client}" class="lbl">CLIENTE:</span> <span class="val"
th:text="${cliente?.nombre} ?: '-'">JUAN JOSÉ th:text="${cliente?.nombre} ?: '-'">JUAN JOSÉ
MÉNDEZ</span></td> MÉNDEZ
</span>
</td>
<td class="text-end"><span class="lbl" th:text="#{pdf.presupuesto.date}">FECHA:</span> <span class="val" <td class="text-end"><span class="lbl" th:text="#{pdf.presupuesto.date}">FECHA:</span> <span class="val"
th:text="${#temporals.format(fecha, 'dd/MM/yyyy')}">10/10/2025</span></td> th:text="${#temporals.format(fecha, 'dd/MM/yyyy')}">10/10/2025</span></td>
</tr> </tr>
@ -69,9 +72,6 @@
</div> </div>
<!-- DATOS TÉCNICOS --> <!-- DATOS TÉCNICOS -->
<div class="line-title">
<span class="lbl" th:text="#{pdf.presupuesto.descripcion}">Descripción:</span>
</div>
<div class="specs-wrapper align-with-text "> <div class="specs-wrapper align-with-text ">
<div class="specs"> <div class="specs">
<div th:if="${specs} == null"> <div th:if="${specs} == null">
@ -85,11 +85,15 @@
<span th:utext="#{pdf.servicios-adicionales}">Servicios adicionales</span> <span th:utext="#{pdf.servicios-adicionales}">Servicios adicionales</span>
<span class="spec-label" th:text="${specs.servicios}"></span> <span class="spec-label" th:text="${specs.servicios}"></span>
</div> </div>
<div th:if="${specs.datosMaquetacion != null}" class="spec-row mb-1"> <div th:if="${specs != null
and #maps.containsKey(specs, 'datosMaquetacion')
and specs['datosMaquetacion'] != null}" class="spec-row mb-1">
<span th:text="#{pdf.datos-maquetacion}">Datos de maquetación:</span> <span th:text="#{pdf.datos-maquetacion}">Datos de maquetación:</span>
<span th:utext="${specs.datosMaquetacion}"></span> <span th:utext="${specs.datosMaquetacion}"></span>
</div> </div>
<div th:if="${specs.datosMarcapaginas != null}" class="spec-row mb-1"> <div th:if="${specs != null
and #maps.containsKey(specs, 'datosMarcapaginas')
and specs['datosMarcapaginas'] != null}" class="spec-row mb-1">
<span th:text="#{pdf.datos-marcapaginas}">Datos de marcapáginas:</span> <span th:text="#{pdf.datos-marcapaginas}">Datos de marcapáginas:</span>
<span th:utext="${specs.datosMarcapaginas}"></span> <span th:utext="${specs.datosMarcapaginas}"></span>
</div> </div>
@ -98,7 +102,7 @@
</div> </div>
<!-- TABLA TIRADAS --> <!-- TABLA TIRADAS -->
<table class="prices" class="align-items-center"> <table class="prices align-items-center">
<thead> <thead>
<tr> <tr>
<th class="text-center col-tirada" th:text="#{pdf.table.tirada}">TIRADA</th> <th class="text-center col-tirada" th:text="#{pdf.table.tirada}">TIRADA</th>
@ -133,6 +137,8 @@
<!-- PIE --> <!-- PIE -->
<div class="footer"> <div class="footer">
<div class="fw-bold fs-6 mb-1" th:text="#{pdf.incluye-envio}">El presupuesto incluye el envío a una dirección de la
península.</div>
<div class="privacy"> <div class="privacy">
<div class="pv-title" th:text="#{pdf.politica-privacidad}">Política de privacidad</div> <div class="pv-title" th:text="#{pdf.politica-privacidad}">Política de privacidad</div>
<div class="pv-text" th:text="#{pdf.politica-privacidad.responsable}">Responsable: Impresión Imprime Libros - <div class="pv-text" th:text="#{pdf.politica-privacidad.responsable}">Responsable: Impresión Imprime Libros -

View File

@ -1,4 +1,4 @@
<div th:fragment="buttons(appMode, btnClass, showPrev, showNext, showActions, showCart)" class="buttons-bar mt-2"> <div th:fragment="buttons(appMode, btnClass, showPrev, showNext, showActions, showCart, showPrint)" class="buttons-bar mt-2">
<!-- Fila 1: ACCIONES, centradas --> <!-- Fila 1: ACCIONES, centradas -->
<div class="buttons-row center" th:if="${showActions}"> <div class="buttons-row center" th:if="${showActions}">
@ -8,7 +8,7 @@
<span th:text="#{presupuesto.guardar}">Guardar</span> <span th:text="#{presupuesto.guardar}">Guardar</span>
</button> </button>
<button type="button" class="btn btn-secondary d-flex align-items-center mx-2 btn-imprimir"> <button th:if="${showPrint}" type="button" class="btn btn-secondary d-flex align-items-center mx-2 btn-imprimir">
<i class="ri-printer-line me-2"></i> <i class="ri-printer-line me-2"></i>
<span th:text="#{app.imprimir}">Imprimir</span> <span th:text="#{app.imprimir}">Imprimir</span>
</button> </button>

View File

@ -408,6 +408,11 @@
<div class="d-flex justify-content-between align-items-center mt-4 w-100"> <div class="d-flex justify-content-between align-items-center mt-4 w-100">
<div th:replace="~{imprimelibros/presupuestos/presupuestador-items/_buttons :: buttons(${appMode}, 'btn-change-tab-cubierta', true, true, true, false)}"></div> <div sec:authorize="isAuthenticated()"
th:replace="~{imprimelibros/presupuestos/presupuestador-items/_buttons :: buttons(${appMode}, 'btn-change-tab-cubierta', true, true, true, false, true)}">
</div>
<div sec:authorize="!isAuthenticated()"
th:replace="~{imprimelibros/presupuestos/presupuestador-items/_buttons :: buttons(${appMode}, 'btn-change-tab-cubierta', true, true, false, false, false)}">
</div>
</div> </div>
</div> </div>

View File

@ -299,7 +299,7 @@
<div <div
th:replace="~{imprimelibros/presupuestos/presupuestador-items/_buttons :: buttons(${appMode}, 'btn-change-tab-datos-generales', false, true, false, false)}"> th:replace="~{imprimelibros/presupuestos/presupuestador-items/_buttons :: buttons(${appMode}, 'btn-change-tab-datos-generales', false, true, false, false, false)}">
</div> </div>

View File

@ -17,6 +17,9 @@
<!-- End Ribbon Shape --> <!-- End Ribbon Shape -->
<div class="d-flex justify-content-between align-items-center mt-4 w-100"> <div class="d-flex justify-content-between align-items-center mt-4 w-100">
<div th:replace="~{imprimelibros/presupuestos/presupuestador-items/_buttons :: buttons(${appMode}, 'btn-change-tab-extras', true, true, true, false)}"></div> <div sec:authorize="isAuthenticated()"
th:replace="~{imprimelibros/presupuestos/presupuestador-items/_buttons :: buttons(${appMode}, 'btn-change-tab-extras', true, true, true, false, true)}"></div>
<div sec:authorize="!isAuthenticated()"
th:replace="~{imprimelibros/presupuestos/presupuestador-items/_buttons :: buttons(${appMode}, 'btn-change-tab-extras', true, true, false, false, false)}"></div>
</div> </div>
</div> </div>

View File

@ -57,8 +57,11 @@
</div> </div>
</div> </div>
<div <div sec:authorize="!isAuthenticated()"
th:replace="~{imprimelibros/presupuestos/presupuestador-items/_buttons :: buttons(${appMode}, 'btn-change-tab-interior', true, true, true, false)}"> th:replace="~{imprimelibros/presupuestos/presupuestador-items/_buttons :: buttons(${appMode}, 'btn-change-tab-interior', true, true, false, false, false)}">
</div>
<div sec:authorize="isAuthenticated()"
th:replace="~{imprimelibros/presupuestos/presupuestador-items/_buttons :: buttons(${appMode}, 'btn-change-tab-interior', true, true, true, false, true)}">
</div> </div>

View File

@ -51,7 +51,7 @@
<div class="d-flex justify-content-between align-items-center mt-4 w-100"> <div class="d-flex justify-content-between align-items-center mt-4 w-100">
<div <div
th:replace="~{imprimelibros/presupuestos/presupuestador-items/_buttons :: buttons(${appMode}, 'btn-change-tab-resumen', true, false, true, true)}"> th:replace="~{imprimelibros/presupuestos/presupuestador-items/_buttons :: buttons(${appMode}, 'btn-change-tab-resumen', true, false, true, true, true)}">
</div> </div>
</div> </div>
</div> </div>

View File

@ -11,12 +11,10 @@
</div> </div>
<div class="ribbon-content mt-4"> <div class="ribbon-content mt-4">
<div id="div-tiradas-error" <div id="div-tiradas-error" class="d-flex justify-content-center mt-4 w-100 d-none">
class="d-flex justify-content-center mt-4 w-100 d-none">
</div> </div>
<div id="div-tiradas" <div id="div-tiradas" class="row row-cols-auto justify-content-center g-4 mx-auto mb-4"
class="row row-cols-auto justify-content-center g-4 mx-auto mb-4"
style="max-width:1120px" th:data-per-unit="#{presupuesto.precio-unidad}" style="max-width:1120px" th:data-per-unit="#{presupuesto.precio-unidad}"
th:data-total="#{presupuesto.total}" th:data-select="#{presupuesto.seleccionar-tirada}" th:data-total="#{presupuesto.total}" th:data-select="#{presupuesto.seleccionar-tirada}"
th:data-selected="#{presupuesto.tirada-seleccionada}" th:data-units="#{presupuesto.unidades}"> th:data-selected="#{presupuesto.tirada-seleccionada}" th:data-units="#{presupuesto.unidades}">
@ -27,6 +25,11 @@
<!-- End Ribbon Shape --> <!-- End Ribbon Shape -->
<div class="d-flex justify-content-between align-items-center mt-4 w-100"> <div class="d-flex justify-content-between align-items-center mt-4 w-100">
<div th:replace="~{imprimelibros/presupuestos/presupuestador-items/_buttons :: buttons(${appMode}, 'btn-change-tab-seleccion-tirada', true, true, true, false)}"></div> <div sec:authorize="isAuthenticated()"
th:replace="~{imprimelibros/presupuestos/presupuestador-items/_buttons :: buttons(${appMode}, 'btn-change-tab-seleccion-tirada', true, true, true, false, true)}">
</div>
<div sec:authorize="!isAuthenticated()"
th:replace="~{imprimelibros/presupuestos/presupuestador-items/_buttons :: buttons(${appMode}, 'btn-change-tab-seleccion-tirada', true, true, false, false, false)}">
</div>
</div> </div>
</div> </div>