mirror of
https://git.imnavajas.es/jjimenez/erp-imprimelibros.git
synced 2026-01-12 16:38:48 +00:00
terminado
This commit is contained in:
6
pom.xml
6
pom.xml
@ -92,6 +92,12 @@
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.jsoup</groupId>
|
||||
<artifactId>jsoup</artifactId>
|
||||
<version>1.17.2</version>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
|
||||
@ -0,0 +1,20 @@
|
||||
package com.imprimelibros.erp.common;
|
||||
|
||||
import jakarta.persistence.AttributeConverter;
|
||||
import jakarta.persistence.Converter;
|
||||
import org.jsoup.Jsoup;
|
||||
import org.jsoup.safety.Safelist;
|
||||
|
||||
@Converter(autoApply = false)
|
||||
public class HtmlStripConverter implements AttributeConverter<String, String> {
|
||||
|
||||
@Override
|
||||
public String convertToDatabaseColumn(String attribute) {
|
||||
return attribute == null ? null : Jsoup.clean(attribute, Safelist.none());
|
||||
}
|
||||
|
||||
@Override
|
||||
public String convertToEntityAttribute(String dbData) {
|
||||
return dbData; // tal cual
|
||||
}
|
||||
}
|
||||
@ -8,6 +8,8 @@ import com.imprimelibros.erp.presupuesto.validation.Par;
|
||||
import com.imprimelibros.erp.presupuesto.validation.PresupuestoValidationGroups;
|
||||
import com.imprimelibros.erp.presupuesto.validation.Tamanio;
|
||||
|
||||
import com.imprimelibros.erp.common.HtmlStripConverter;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
|
||||
@ConsistentTiradas(groups = PresupuestoValidationGroups.DatosGenerales.class)
|
||||
@ -45,13 +47,16 @@ public class Presupuesto implements Cloneable{
|
||||
@Column(name = "tipo_encuadernacion")
|
||||
private TipoEncuadernacion tipoEncuadernacion = TipoEncuadernacion.fresado;
|
||||
|
||||
@Convert(converter = HtmlStripConverter.class)
|
||||
@NotBlank(message = "{presupuesto.errores.titulo}", groups = PresupuestoValidationGroups.DatosGenerales.class)
|
||||
@Column(name = "titulo")
|
||||
private String titulo;
|
||||
|
||||
@Convert(converter = HtmlStripConverter.class)
|
||||
@Column(name = "autor")
|
||||
private String autor;
|
||||
|
||||
@Convert(converter = HtmlStripConverter.class)
|
||||
@Column(name = "isbn")
|
||||
private String isbn;
|
||||
|
||||
|
||||
@ -1,9 +1,11 @@
|
||||
presupuesto.datos-generales=Datos Generales
|
||||
presupuesto.interior=Interior
|
||||
presupuesto.cubierta=Cubierta
|
||||
presupuesto.seleccion-tirada=Seleccion de tirada
|
||||
presupuesto.seleccion-tirada=Selección tirada
|
||||
presupuesto.extras=Extras
|
||||
presupuesto.resumen=Resumen
|
||||
presupuesto.add-to-presupuesto=Añadir al presupuesto
|
||||
presupuesto.calcular=Calcular
|
||||
|
||||
# Pestaña datos generales de presupuesto
|
||||
presupuesto.informacion-libro=Información del libro
|
||||
@ -153,7 +155,9 @@ presupuesto.volver-cubierta=Volver a diseño cubierta
|
||||
presupuesto.finalizar=Finalizar presupuesto
|
||||
presupuesto.calcular-presupuesto=Calcular presupuesto
|
||||
presupuesto.consultar-soporte=Consultar con soporte
|
||||
presupuesto.calcular=Calcular
|
||||
|
||||
# Pestaña resumen del presupuesto
|
||||
presupuesto.volver-extras=Volver a extras
|
||||
|
||||
# Resumen del presupuesto
|
||||
presupuesto.resumen-presupuesto=Resumen presupuesto
|
||||
|
||||
@ -70,7 +70,7 @@ class PresupuestoCliente {
|
||||
acabado_marcapaginas: 'ninguno',
|
||||
resultado: {
|
||||
precio_unitario: 0,
|
||||
precio_total: 0
|
||||
precio: 0
|
||||
}
|
||||
},
|
||||
datosMaquetacion: {
|
||||
@ -184,6 +184,7 @@ class PresupuestoCliente {
|
||||
this.#initInterior();
|
||||
this.#initSeleccionTirada();
|
||||
this.#initExtras();
|
||||
this.#initResumen();
|
||||
|
||||
$(document).on('change', 'input[min][max]', function () {
|
||||
const $input = $(this);
|
||||
@ -1387,14 +1388,25 @@ class PresupuestoCliente {
|
||||
this.#changeTab('pills-seleccion-tirada');
|
||||
this.summaryTableExtras.addClass('d-none');
|
||||
} else {
|
||||
//this.#changeTab('pills-finalizar');
|
||||
this.#changeTab('pills-resumen');
|
||||
}
|
||||
});
|
||||
|
||||
// Eventos para el resumen
|
||||
$(document).on('change', '.service-checkbox', (e) => {
|
||||
Summary.updateExtras();
|
||||
|
||||
const $target = $(e.currentTarget);
|
||||
|
||||
if ($target.prop('checked')) {
|
||||
this.formData.servicios.servicios.push($target.val());
|
||||
} else {
|
||||
const index = this.formData.servicios.servicios.indexOf($target.val());
|
||||
if (index > -1) {
|
||||
this.formData.servicios.servicios.splice(index, 1);
|
||||
}
|
||||
}
|
||||
Summary.updateExtras();
|
||||
this.#cacheFormData();
|
||||
});
|
||||
}
|
||||
|
||||
@ -1406,14 +1418,44 @@ class PresupuestoCliente {
|
||||
|
||||
this.divExtras.addClass('animate-fadeInUpBounce');
|
||||
for (const extra of servicios) {
|
||||
if (this.formData.servicios.servicios.includes(extra.id) && !extra.checked) {
|
||||
extra.checked = true;
|
||||
if (extra.id === "marcapaginas" || extra.id === "maquetacion") {
|
||||
extra.price = extra.id === "marcapaginas" ?
|
||||
this.formData.servicios.datosMarcapaginas.resultado.precio :
|
||||
this.formData.servicios.datosMaquetacion.resultado.precio;
|
||||
extra.priceUnit = this.divExtras.data('currency');
|
||||
}
|
||||
}
|
||||
const item = new ServiceOptionCard(extra);
|
||||
this.divExtras.append(item.render());
|
||||
if (item.checked) {
|
||||
if (!this.formData.servicios.servicios.includes(extra.id)) {
|
||||
this.formData.servicios.servicios.push(extra.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.#cacheFormData();
|
||||
Summary.updateExtras();
|
||||
}
|
||||
/******************************
|
||||
* END EXTRAS
|
||||
******************************/
|
||||
|
||||
|
||||
/******************************
|
||||
* EXTRAS
|
||||
******************************/
|
||||
#initResumen() {
|
||||
|
||||
$(document).on('click', '.btn-change-tab-resumen', (e) => {
|
||||
const id = e.currentTarget.id;
|
||||
if (id === 'btn-prev-resumen') {
|
||||
this.#changeTab('pills-extras');
|
||||
}
|
||||
});
|
||||
}
|
||||
/******************************
|
||||
* END EXTRAS
|
||||
******************************/
|
||||
|
||||
@ -113,9 +113,11 @@ $(document).on("submit", "#maquetacionForm", function (e) {
|
||||
$(document).on('hidden.bs.modal', '#maquetacionModal', function () {
|
||||
|
||||
const calcularStr = $('#div-extras').data('language-calcular');
|
||||
$('#maquetacion').prop('checked', false);
|
||||
$('#maquetacion').data('price', calcularStr);
|
||||
$('label[for="maquetacion"] .service-price').text(calcularStr);
|
||||
$('#maquetacion').prop('checked', false).trigger('change');
|
||||
|
||||
Summary.updateExtras();
|
||||
});
|
||||
|
||||
|
||||
@ -138,6 +140,8 @@ function loadMaquetacionData() {
|
||||
$('#texto-mecanografiado').prop('checked', stored.texto_mecanografiado);
|
||||
$('#disenio-portada').prop('checked', stored.disenio_portada);
|
||||
$('#epub').prop('checked', stored.epub);
|
||||
|
||||
Summary.updateExtras();
|
||||
}
|
||||
|
||||
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import * as Summary from "./summary.js";
|
||||
import { formateaMoneda, formateaMoneda6Decimales } from "../utils.js";
|
||||
import { formateaMoneda } from "../utils.js";
|
||||
|
||||
$(document).on('change', '#marcapaginas', function (e) {
|
||||
e.preventDefault();
|
||||
@ -10,7 +10,7 @@ $(document).on('change', '#marcapaginas', function (e) {
|
||||
loadMarcapaginasData();
|
||||
|
||||
// init marcapaginas form
|
||||
filtrarAcabados();
|
||||
filtrarAcabados();
|
||||
|
||||
$("#marcapaginasModal").modal("show");
|
||||
});
|
||||
@ -42,7 +42,7 @@ $(document).on("submit", "#marcapaginasForm", function (e) {
|
||||
|
||||
const resumenHtml = `
|
||||
<div class="text-start">
|
||||
<p>${json.language.precio_unidad || 'Precio por unidad'}: ${formateaMoneda6Decimales(json.precio_unitario) || "-"}</p>
|
||||
<p>${json.language.precio_unidad || 'Precio por unidad'}: ${formateaMoneda(json.precio_unitario, 6) || "-"}</p>
|
||||
<h3 class="mb-0">${json.language.precio_total || 'Precio total'}: ${formateaMoneda(json.precio_total) || "-"}</h3>
|
||||
</div>
|
||||
`;
|
||||
@ -72,7 +72,7 @@ $(document).on("submit", "#marcapaginasForm", function (e) {
|
||||
// guardamos los datos del formulario en sessionStorage
|
||||
const stored = JSON.parse(sessionStorage.getItem("formData"));
|
||||
stored.servicios.datosMarcapaginas.resultado.precio_unitario = json.precio_unitario;
|
||||
stored.servicios.datosMarcapaginas.resultado.precio_total = json.precio_total;
|
||||
stored.servicios.datosMarcapaginas.resultado.precio = json.precio_total;
|
||||
sessionStorage.setItem("formData", JSON.stringify(stored));
|
||||
}
|
||||
else {
|
||||
@ -117,9 +117,10 @@ $(document).on("change", "#caras-impresion", function (e) {
|
||||
$(document).on('hidden.bs.modal', '#marcapaginasModal', function () {
|
||||
|
||||
const calcularStr = $('#div-extras').data('language-calcular');
|
||||
$('#marcapaginas').prop('checked', false);
|
||||
$('#marcapaginas').data('price', calcularStr);
|
||||
$('label[for="marcapaginas"] .service-price').text(calcularStr);
|
||||
$('#marcapaginas').prop('checked', false).trigger('change');
|
||||
Summary.updateExtras();
|
||||
});
|
||||
|
||||
|
||||
|
||||
@ -1,3 +1,5 @@
|
||||
import {formateaMoneda, isNumber} from '../utils.js';
|
||||
|
||||
class ServiceOptionCard {
|
||||
|
||||
constructor({ id, title, description = '', price = '', priceUnit = '', checked = false, allowChange = true, ribbonText }) {
|
||||
@ -23,7 +25,7 @@ class ServiceOptionCard {
|
||||
${ribbonHtml}
|
||||
<h5 class="service-title mb-1">${this.title}</h5>
|
||||
<p class="service-desc mb-1 small">${this.description}</p>
|
||||
<h4 class="service-price fw-semibold mt-2 h4 mb-0">${this.price} ${this.priceUnit}</h4>
|
||||
<h4 class="service-price fw-semibold mt-2 h4 mb-0">${isNumber(this.price) ? formateaMoneda(this.price, 2, this.locale) : this.price}</h4>
|
||||
</label>
|
||||
</div>
|
||||
`);
|
||||
|
||||
@ -1,6 +1,8 @@
|
||||
import { formateaMoneda, formateaNumero } from "../utils.js";
|
||||
|
||||
// ===== Clase =====
|
||||
class TiradaCard {
|
||||
constructor({ id, titulo, unidades, precioUnidad, precioTotal, selected = false, moneda = '€', name = 'tirada',
|
||||
constructor({ id, titulo, unidades, precioUnidad, precioTotal, selected = false, name = 'tirada',
|
||||
labels = {}, locale = (document.documentElement.lang || navigator.language || 'es-ES')
|
||||
}) {
|
||||
this.id = id;
|
||||
@ -9,7 +11,6 @@ class TiradaCard {
|
||||
this.precioUnidad = precioUnidad;
|
||||
this.precioTotal = precioTotal;
|
||||
this.selected = selected;
|
||||
this.moneda = moneda;
|
||||
this.name = name;
|
||||
this.locale = locale;
|
||||
this.labels = Object.assign({
|
||||
@ -22,16 +23,10 @@ class TiradaCard {
|
||||
this.$container = null; // se establece al renderizar
|
||||
}
|
||||
|
||||
#formatMoneyES(value, decimals = 2) {
|
||||
return new Intl.NumberFormat(this.locale, {
|
||||
minimumFractionDigits: decimals,
|
||||
maximumFractionDigits: decimals
|
||||
}).format(Number(value));
|
||||
}
|
||||
|
||||
#title() {
|
||||
if (this.titulo) return this.titulo;
|
||||
if (this.unidades != null) return `${this.unidades} ${this.labels.units}`;
|
||||
if (this.unidades != null) return `${formateaNumero({valor: this.unidades, locale: this.locale, digits: 0})} ${this.labels.units}`;
|
||||
return '';
|
||||
}
|
||||
|
||||
@ -46,11 +41,11 @@ class TiradaCard {
|
||||
<div class="title">${this.#title()}</div>
|
||||
|
||||
<div class="per muted">${this.labels.perUnit}</div>
|
||||
<div class="price-big">${this.#formatMoneyES(this.precioUnidad, 4)} ${this.moneda}</div>
|
||||
<div class="price-big">${formateaMoneda(this.precioUnidad, 4, this.locale)}</div>
|
||||
|
||||
<div class="price-row">
|
||||
<div class="muted">${this.labels.total}</div>
|
||||
<div class="price-total fw-bold">${this.#formatMoneyES(this.precioTotal, 2)} ${this.moneda}</div>
|
||||
<div class="price-total fw-bold">${formateaMoneda(this.precioTotal, 2, this.locale)}</div>
|
||||
</div>
|
||||
|
||||
<button type="button" class="btn btn-select-tirada">${this.labels.select}</button>
|
||||
|
||||
@ -1,26 +1,40 @@
|
||||
function formateaMoneda(valor) {
|
||||
function formateaMoneda(valor, digits = 2, locale = 'es-ES', currency = 'EUR') {
|
||||
try {
|
||||
return new Intl.NumberFormat('es-ES', { style: 'currency', currency: 'EUR' }).format(valor);
|
||||
return new Intl.NumberFormat(locale, { style: 'currency', currency, minimumFractionDigits: digits, useGrouping: true }).format(valor);
|
||||
} catch {
|
||||
return valor;
|
||||
}
|
||||
}
|
||||
export { formateaMoneda };
|
||||
|
||||
function formateaMoneda4Decimales(valor) {
|
||||
try {
|
||||
return new Intl.NumberFormat('es-ES', { style: 'currency', currency: 'EUR', minimumFractionDigits: 4 }).format(valor);
|
||||
} catch {
|
||||
return valor;
|
||||
}
|
||||
}
|
||||
export { formateaMoneda4Decimales };
|
||||
|
||||
function formateaMoneda6Decimales(valor) {
|
||||
try {
|
||||
return new Intl.NumberFormat('es-ES', { style: 'currency', currency: 'EUR', minimumFractionDigits: 6 }).format(valor);
|
||||
} catch {
|
||||
return valor;
|
||||
function formateaNumero({
|
||||
valor,
|
||||
digits = 2,
|
||||
style = 'decimal',
|
||||
locale = 'es-ES',
|
||||
currency = 'EUR'
|
||||
}) {
|
||||
const n = Number(valor);
|
||||
if (!Number.isFinite(n)) return valor;
|
||||
|
||||
const opts = {
|
||||
useGrouping: true,
|
||||
minimumFractionDigits: digits,
|
||||
maximumFractionDigits: digits,
|
||||
};
|
||||
|
||||
if (style === 'currency') {
|
||||
opts.style = 'currency';
|
||||
opts.currency = currency;
|
||||
}
|
||||
|
||||
return new Intl.NumberFormat(locale, opts).format(n);
|
||||
}
|
||||
export { formateaMoneda6Decimales };
|
||||
export { formateaNumero };
|
||||
|
||||
|
||||
function isNumber(value) {
|
||||
return !isNaN(Number(value)) && value.trim() !== '';
|
||||
}
|
||||
export { isNumber };
|
||||
@ -24,7 +24,7 @@
|
||||
<label for="titulo" class="form-label" th:text="#{presupuesto.titulo}">
|
||||
>Título*</label>
|
||||
<input type="text" class="form-control datos-generales-data" id="titulo" placeholder=""
|
||||
value="">
|
||||
th:value="${presupuesto?.titulo} ?: ''">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -33,13 +33,13 @@
|
||||
<div class="col-sm-6">
|
||||
<div class="mb-3">
|
||||
<label for="autor" class="form-label" th:text="#{presupuesto.autor}">Autor</label>
|
||||
<input type="text" class="form-control datos-generales-data" id="autor" value="">
|
||||
<input type="text" class="form-control datos-generales-data" id="autor" th:value="${presupuesto?.autor} ?: ''">
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-6">
|
||||
<div class="mb-3">
|
||||
<label for="isbn" class="form-label" th:text="#{presupuesto.isbn}">ISBN</label>
|
||||
<input type="text" class="form-control datos-generales-data" id="isbn" value="">
|
||||
<input type="text" class="form-control datos-generales-data" id="isbn" th:value="${presupuesto?.isbn} ?: ''" >
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -10,7 +10,7 @@
|
||||
</div>
|
||||
|
||||
<div class="ribbon-content mt-4">
|
||||
<div id="div-extras" class="hstack gap-2 justify-content-center flex-wrap">
|
||||
<div id="div-extras" class="hstack gap-2 justify-content-center flex-wrap" th:data-currency="#{app.currency-symbol}">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -24,7 +24,7 @@
|
||||
</button>
|
||||
<button id="btn-next-extras" type="button"
|
||||
class="btn btn-secondary d-flex align-items-center btn-change-tab-extras">
|
||||
<span><b th:text="#{presupuesto.calcular-presupuesto}">Calcular presupuesto</b></span>
|
||||
<span><b th:text="#{presupuesto.resumen}">Resumen</b></span>
|
||||
<i class="ri-arrow-right-circle-line fs-16 ms-2"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@ -0,0 +1,24 @@
|
||||
<div class="animate-fadeInUpBounce">
|
||||
|
||||
<!-- Ribbon Shape -->
|
||||
<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="#{presupuesto.resumen}">Resumen
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="ribbon-content mt-4">
|
||||
<div id="div-extras" class="hstack gap-2 justify-content-center flex-wrap">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- End Ribbon Shape -->
|
||||
|
||||
<div class="d-flex justify-content-between align-items-center mt-4 w-100">
|
||||
<button id="btn-prev-resumen" type="button"
|
||||
class="btn btn-light d-flex align-items-center btn-change-tab-resumen">
|
||||
<i class=" ri-arrow-left-circle-line label-icon align-middle fs-16 me-2"></i>
|
||||
<span th:text="#{presupuesto.volver-extras}">Volver a extras</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@ -61,6 +61,15 @@
|
||||
<label th:text="#{presupuesto.extras}">Extras</label>
|
||||
</button>
|
||||
</li>
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link fs-15 p-3" id="pills-resumen-tab"
|
||||
data-bs-target="#pills-resumen" type="button" role="tab"
|
||||
aria-controls="pills-resumen" aria-selected="false">
|
||||
<i
|
||||
class="ri-add-box-line fs-16 p-2 bg-soft-primary text-primary rounded-circle align-middle me-2"></i>
|
||||
<label th:text="#{presupuesto.resumen}">Resumen</label>
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
@ -106,6 +115,13 @@
|
||||
</div>
|
||||
<!-- end tab pane -->
|
||||
|
||||
<div class="tab-pane fade" id="pills-resumen" role="tabpanel"
|
||||
aria-labelledby="pills-resumen-tab">
|
||||
|
||||
<div th:include="~{imprimelibros/presupuestos/presupuestador-items/_resumen.html}"></div>
|
||||
</div>
|
||||
<!-- end tab pane -->
|
||||
|
||||
</div>
|
||||
<!-- end tab content -->
|
||||
</form>
|
||||
|
||||
Reference in New Issue
Block a user