terminado

This commit is contained in:
Jaime Jiménez
2025-09-11 12:15:56 +02:00
parent 6a9c197a02
commit 67b5f9457e
15 changed files with 311 additions and 91 deletions

View File

@ -22,15 +22,20 @@ public class InternationalizationConfig implements WebMvcConfigurer {
@Bean @Bean
public LocaleResolver localeResolver() { public LocaleResolver localeResolver() {
SessionLocaleResolver slr = new SessionLocaleResolver(); SessionLocaleResolver slr = new SessionLocaleResolver();
slr.setDefaultLocale(Locale.of("es")); slr.setDefaultLocale(Locale.forLanguageTag("es")); // idioma por defecto
return slr; return slr;
} }
@Bean
public LocaleChangeInterceptor localeChangeInterceptor() {
LocaleChangeInterceptor lci = new LocaleChangeInterceptor();
lci.setParamName("lang"); // parámetro ?lang=en, ?lang=es
return lci;
}
@Override @Override
public void addInterceptors(InterceptorRegistry registry) { public void addInterceptors(InterceptorRegistry registry) {
LocaleChangeInterceptor interceptor = new LocaleChangeInterceptor(); registry.addInterceptor(localeChangeInterceptor());
interceptor.setParamName("lang");
registry.addInterceptor(interceptor);
} }
@Bean @Bean
@ -40,22 +45,22 @@ public class InternationalizationConfig implements WebMvcConfigurer {
PathMatchingResourcePatternResolver resolver = new PathMatchingResourcePatternResolver(); PathMatchingResourcePatternResolver resolver = new PathMatchingResourcePatternResolver();
Resource[] resources = resolver.getResources("classpath*:i18n/*.properties"); Resource[] resources = resolver.getResources("classpath*:i18n/*.properties");
// Extraer los nombres base sin extensión ni sufijos (_en, _es, etc.) // Extraer nombres base sin sufijos de idioma
Set<String> basenames = Arrays.stream(resources) Set<String> basenames = Arrays.stream(resources)
.map(res -> { .map(res -> {
try { try {
String uri = Objects.requireNonNull(res.getURI()).toString(); String uri = Objects.requireNonNull(res.getURI()).toString();
// Ej: file:/.../i18n/login_en.properties // Ejemplo: file:/.../i18n/login_en.properties
String path = uri.substring(uri.indexOf("/i18n/") + 1); // i18n/login_en.properties String path = uri.substring(uri.indexOf("/i18n/") + 1); // i18n/login_en.properties
String base = path.replaceAll("_[a-z]{2}\\.properties$", "") // login.properties String base = path.replaceAll("_[a-z]{2}\\.properties$", "") // login.properties
.replaceAll("\\.properties$", ""); .replaceAll("\\.properties$", "");
return "classpath:" + base; return "classpath:" + base;
} catch (IOException e) { } catch (IOException e) {
return null; return null;
} }
}) })
.filter(Objects::nonNull) .filter(Objects::nonNull)
.collect(Collectors.toSet()); .collect(Collectors.toSet());
source.setBasenames(basenames.toArray(new String[0])); source.setBasenames(basenames.toArray(new String[0]));
source.setDefaultEncoding("UTF-8"); source.setDefaultEncoding("UTF-8");

View File

@ -16,6 +16,7 @@ import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.http.MediaType; import org.springframework.http.MediaType;
@ -27,6 +28,8 @@ import com.imprimelibros.erp.presupuesto.classes.ImagenPresupuesto;
import com.imprimelibros.erp.presupuesto.classes.PresupuestoMaquetacion; import com.imprimelibros.erp.presupuesto.classes.PresupuestoMaquetacion;
import com.imprimelibros.erp.presupuesto.validation.PresupuestoValidationGroups; import com.imprimelibros.erp.presupuesto.validation.PresupuestoValidationGroups;
import jakarta.validation.Valid;
@Controller @Controller
@RequestMapping("/presupuesto") @RequestMapping("/presupuesto")
public class PresupuestoController { public class PresupuestoController {
@ -93,7 +96,7 @@ public class PresupuestoController {
@PostMapping("/public/validar/cubierta") @PostMapping("/public/validar/cubierta")
public ResponseEntity<?> validarCubierta( public ResponseEntity<?> validarCubierta(
@Validated(PresupuestoValidationGroups.Cubierta.class) Presupuesto presupuesto, @Validated(PresupuestoValidationGroups.Cubierta.class) Presupuesto presupuesto,
BindingResult result, BindingResult result,
@RequestParam(name = "calcular", defaultValue = "true") boolean calcular, @RequestParam(name = "calcular", defaultValue = "true") boolean calcular,
Locale locale) { Locale locale) {
@ -150,6 +153,9 @@ public class PresupuestoController {
Map<String, Object> resultado = new HashMap<>(); Map<String, Object> resultado = new HashMap<>();
// servicios extra // servicios extra
resultado.putAll(presupuestoService.obtenerServiciosExtras(presupuesto, locale, apiClient)); resultado.putAll(presupuestoService.obtenerServiciosExtras(presupuesto, locale, apiClient));
Map<String, String> language = new HashMap<>();
language.put("calcular", messageSource.getMessage("presupuesto.calcular", null, locale));
resultado.put("language", language);
return ResponseEntity.ok(resultado); return ResponseEntity.ok(resultado);
} }
@ -307,23 +313,35 @@ public class PresupuestoController {
} }
@GetMapping(value="/public/maquetacion/form", produces = MediaType.TEXT_HTML_VALUE) @GetMapping(value = "/public/maquetacion/form", produces = MediaType.TEXT_HTML_VALUE)
public String getMaquetacionForm(Model model) { public String getMaquetacionForm(Model model) {
model.addAttribute("presupuestoMaquetacion", new PresupuestoMaquetacion()); model.addAttribute("presupuestoMaquetacion", new PresupuestoMaquetacion());
return "imprimelibros/presupuestos/presupuesto-maquetacion-form :: maquetacionForm"; return "imprimelibros/presupuestos/presupuesto-maquetacion-form :: maquetacionForm";
} }
@GetMapping("/public/maquetacion") @GetMapping("/public/maquetacion")
public ResponseEntity<?> getPresupuestoMaquetacion( public ResponseEntity<?> getPresupuestoMaquetacion(
PresupuestoMaquetacion presupuestoMaquetacion, @Valid @ModelAttribute PresupuestoMaquetacion presupuestoMaquetacion,
Model model, Locale locale) { BindingResult result,
Map<String, Object> resultado = presupuestoService.getPrecioMaquetacion(presupuestoMaquetacion); Locale locale) {
if((Double)resultado.get("precio") == 0.0 && (Integer)resultado.get("numPaginasEstimadas") == 0
&& (Double)resultado.get("precioPaginaEstimado") == 0.0){ if (result.hasErrors()) {
return ResponseEntity.badRequest().body(messageSource.getMessage("presupuesto.errores.presupuesto-maquetacion", null, locale)); // Construimos un mapa field -> mensaje para tu AJAX
} Map<String, String> errores = result.getFieldErrors().stream()
return ResponseEntity.ok(resultado); .collect(java.util.stream.Collectors.toMap(
fe -> fe.getField(),
fe -> fe.getDefaultMessage(),
(a, b) -> a));
return ResponseEntity.badRequest().body(errores);
}
Map<String, Object> resultado = presupuestoService.getPrecioMaquetacion(presupuestoMaquetacion, locale);
if ((Double) resultado.get("precio") == 0.0 && (Integer) resultado.get("numPaginasEstimadas") == 0
&& (Double) resultado.get("precioPaginaEstimado") == 0.0) {
return ResponseEntity.badRequest()
.body(messageSource.getMessage("presupuesto.errores.presupuesto-maquetacion", null, locale));
}
return ResponseEntity.ok(resultado);
} }
} }

View File

@ -593,7 +593,7 @@ public class PresupuestoService {
return price_prototipo; return price_prototipo;
} }
public HashMap<String, Object> getPrecioMaquetacion(PresupuestoMaquetacion presupuestoMaquetacion) { public HashMap<String, Object> getPrecioMaquetacion(PresupuestoMaquetacion presupuestoMaquetacion, Locale locale) {
try { try {
List<MaquetacionPrecios> lista = maquetacionPreciosRepository.findAll(); List<MaquetacionPrecios> lista = maquetacionPreciosRepository.findAll();
@ -657,6 +657,11 @@ public class PresupuestoService {
out.put("precio", precioRedondeado.doubleValue()); out.put("precio", precioRedondeado.doubleValue());
out.put("numPaginasEstimadas", numPaginas); out.put("numPaginasEstimadas", numPaginas);
out.put("precioPaginaEstimado", precioPaginaEstimado); out.put("precioPaginaEstimado", precioPaginaEstimado);
HashMap<String, String> language = new HashMap<>();
language.put("add_to_presupuesto", messageSource.getMessage("presupuesto.add-to-presupuesto", null, locale));
language.put("cancel", messageSource.getMessage("app.cancelar", null, locale));
language.put("presupuesto_maquetacion", messageSource.getMessage("presupuesto.maquetacion", null, locale));
out.put("language", language);
return out; return out;
} catch (Exception e) { } catch (Exception e) {

View File

@ -1,25 +1,36 @@
package com.imprimelibros.erp.presupuesto.classes; package com.imprimelibros.erp.presupuesto.classes;
import com.imprimelibros.erp.presupuesto.maquetacion.MaquetacionMatrices.FontSize; import com.imprimelibros.erp.presupuesto.maquetacion.MaquetacionMatrices.FontSize;
import com.imprimelibros.erp.presupuesto.maquetacion.MaquetacionMatrices.Formato;; import com.imprimelibros.erp.presupuesto.maquetacion.MaquetacionMatrices.Formato;
import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotNull;
public class PresupuestoMaquetacion { public class PresupuestoMaquetacion {
private int numCaracteres = 200000; @NotNull(message = "{validation.required}")
@Min(value = 1, message = "{validation.min}")
private Integer numCaracteres = 200000;
private Formato formato = Formato.A5; private Formato formato = Formato.A5;
private FontSize cuerpoTexto = FontSize.medium; private FontSize cuerpoTexto = FontSize.medium;
private int numTablas = 0; @Min(value = 0, message = "{validation.min}")
private int numColumnas = 1; @NotNull(message = "{validation.required}")
private int numFotos = 0; private Integer numTablas = 0;
@Min(value = 1, message = "{validation.min}")
@NotNull(message = "{validation.required}")
private Integer numColumnas = 1;
@Min(value = 0, message = "{validation.min}")
@NotNull(message = "{validation.required}")
private Integer numFotos = 0;
private boolean correccionOrtotipografica = false; private boolean correccionOrtotipografica = false;
private boolean textoMecanografiado = false; private boolean textoMecanografiado = false;
private boolean disenioPortada = false; private boolean disenioPortada = false;
private boolean epub = false; private boolean epub = false;
public int getNumCaracteres() { public Integer getNumCaracteres() {
return numCaracteres; return numCaracteres;
} }
public void setNumCaracteres(int numCaracteres) { public void setNumCaracteres(Integer numCaracteres) {
this.numCaracteres = numCaracteres; this.numCaracteres = numCaracteres;
} }
public Formato getFormato() { public Formato getFormato() {
@ -34,22 +45,22 @@ public class PresupuestoMaquetacion {
public void setCuerpoTexto(FontSize cuerpoTexto) { public void setCuerpoTexto(FontSize cuerpoTexto) {
this.cuerpoTexto = cuerpoTexto; this.cuerpoTexto = cuerpoTexto;
} }
public int getNumTablas() { public Integer getNumTablas() {
return numTablas; return numTablas;
} }
public void setNumTablas(int numTablas) { public void setNumTablas(Integer numTablas) {
this.numTablas = numTablas; this.numTablas = numTablas;
} }
public int getNumColumnas() { public Integer getNumColumnas() {
return numColumnas; return numColumnas;
} }
public void setNumColumnas(int numColumnas) { public void setNumColumnas(Integer numColumnas) {
this.numColumnas = numColumnas; this.numColumnas = numColumnas;
} }
public int getNumFotos() { public Integer getNumFotos() {
return numFotos; return numFotos;
} }
public void setNumFotos(int numFotos) { public void setNumFotos(Integer numFotos) {
this.numFotos = numFotos; this.numFotos = numFotos;
} }
public boolean isCorreccionOrtotipografica() { public boolean isCorreccionOrtotipografica() {

View File

@ -1 +1,9 @@
app.currency-symbol= app.currency-symbol=
app.yes=Yes
app.no=No
app.aceptar=Accept
app.cancelar=Cancel
app.guardar=Save
app.editar=Edit
app.eliminar=Delete
app.imprimir=Print

View File

@ -1,3 +1,9 @@
app.currency-symbol= app.currency-symbol=
app.yes= app.yes=
app.no=No app.no=No
app.aceptar=Aceptar
app.cancelar=Cancelar
app.guardar=Guardar
app.editar=Editar
app.eliminar=Eliminar
app.imprimir=Imprimir

View File

@ -3,6 +3,7 @@ presupuesto.interior=Interior
presupuesto.cubierta=Cubierta presupuesto.cubierta=Cubierta
presupuesto.seleccion-tirada=Seleccion de tirada presupuesto.seleccion-tirada=Seleccion de tirada
presupuesto.extras=Extras presupuesto.extras=Extras
presupuesto.add-to-presupuesto=Añadir al presupuesto
# Pestaña datos generales de presupuesto # Pestaña datos generales de presupuesto
presupuesto.informacion-libro=Información del libro presupuesto.informacion-libro=Información del libro
@ -148,6 +149,7 @@ presupuesto.volver-cubierta=Volver a diseño cubierta
presupuesto.finalizar=Finalizar presupuesto presupuesto.finalizar=Finalizar presupuesto
presupuesto.calcular-presupuesto=Calcular presupuesto presupuesto.calcular-presupuesto=Calcular presupuesto
presupuesto.consultar-soporte=Consultar con soporte presupuesto.consultar-soporte=Consultar con soporte
presupuesto.calcular=Calcular
# Resumen del presupuesto # Resumen del presupuesto
presupuesto.resumen-presupuesto=Resumen presupuesto presupuesto.resumen-presupuesto=Resumen presupuesto
@ -160,6 +162,7 @@ presupuesto.paginas=Páginas
presupuesto.solapas=Solapas presupuesto.solapas=Solapas
presupuesto.papel-gramaje=Papel y gramaje presupuesto.papel-gramaje=Papel y gramaje
# Presupuesto de maquetación
presupuesto.maquetacion=Presupuesto de maquetación presupuesto.maquetacion=Presupuesto de maquetación
presupuesto.maquetacion.num-caracteres=Número de caracteres presupuesto.maquetacion.num-caracteres=Número de caracteres
presupuesto.maquetacion.num-caracteres-descripcion=Caracteres con espacios (obtenidos desde Word) presupuesto.maquetacion.num-caracteres-descripcion=Caracteres con espacios (obtenidos desde Word)

View File

@ -0,0 +1,6 @@
validation.required=Required field
validation.number=The field must be a valid number
validation.min=The minimum value is {value}
validation.max=The maximum value is {value}
validation.typeMismatchMsg=Invalid data type
validation.patternMsg=Invalid format

View File

@ -0,0 +1,8 @@
validation.required=El campo es obligatorio
validation.number=El campo debe ser un número válido
validation.min=El valor mínimo es {value}
validation.max=El valor máximo es {value}
validation.typeMismatchMsg=Tipo de dato no válido
validation.patternMsg=El formato no es válido

View File

@ -1,37 +1,54 @@
(function () { (function () {
"use strict"; "use strict";
const default_lang = "es"; const DEFAULT_LANG = "es";
const language = localStorage.getItem("language");
function initLanguage() { function getCurrentLang() {
const saved = localStorage.getItem("language") || default_lang; // Viene del servidor (Thymeleaf): <html th:lang="${#locale.language}">
setLanguage(saved, false); // solo actualiza bandera y lang return document.documentElement.lang || DEFAULT_LANG;
document.querySelectorAll('.language').forEach(a => { }
a.addEventListener('click', () => setLanguage(a.dataset.lang, true));
});
}
function setLanguage(lang, redirect = true) { function setFlag(lang) {
const already = document.documentElement.lang === lang; const img = document.getElementById("header-lang-img");
if (!img) return;
img.src = (lang === "en")
? "/assets/images/flags/gb.svg"
: "/assets/images/flags/spain.svg";
}
// Actualiza <html lang> y bandera function onLanguageClick(e) {
document.documentElement.lang = lang; e.preventDefault();
document.getElementById("header-lang-img").src = const lang = this.dataset.lang;
lang === "en" ? "/assets/images/flags/gb.svg" if (!lang || lang === getCurrentLang()) return;
: "/assets/images/flags/spain.svg";
localStorage.setItem("language", lang);
// Redirige si cambia el idioma // Guarda la preferencia (opcional)
if (!already && redirect) { try { localStorage.setItem("language", lang); } catch {}
const url = new URL(location.href);
url.searchParams.set("lang", lang);
location.href = url.toString();
}
}
// Llama al inicializador de idioma en cuanto el DOM esté listo // Redirige con ?lang=... para que Spring cambie el Locale y renderice en ese idioma
document.addEventListener("DOMContentLoaded", function () { const url = new URL(location.href);
initLanguage(); url.searchParams.set("lang", lang);
}); location.assign(url);
}
function initLanguage() {
// Usa el idioma actual que viene del servidor
const current = getCurrentLang();
setFlag(current);
// Enlaces/ botones de idioma: .language[data-lang="en|es"]
document.querySelectorAll(".language").forEach(a => {
a.addEventListener("click", onLanguageClick);
});
// (Opcional) si guardaste algo en localStorage y quieres forzar
// la alineación al entrar por 1ª vez:
const saved = localStorage.getItem("language");
if (saved && saved !== current) {
const url = new URL(location.href);
url.searchParams.set("lang", saved);
location.replace(url); // alinea y no deja historial extra
}
}
document.addEventListener("DOMContentLoaded", initLanguage);
})(); })();

View File

@ -129,6 +129,7 @@ class PresupuestoCliente {
this.summaryTableCubierta = $('#summary-cubierta'); this.summaryTableCubierta = $('#summary-cubierta');
this.summaryTableSobrecubierta = $('#summary-sobrecubierta'); this.summaryTableSobrecubierta = $('#summary-sobrecubierta');
this.summaryTableFaja = $('#summary-faja'); this.summaryTableFaja = $('#summary-faja');
this.summaryTableExtras = $('#summary-servicios-extras');
} }
init() { init() {
@ -1280,6 +1281,7 @@ class PresupuestoCliente {
type: 'POST', type: 'POST',
data: data, data: data,
success: (data) => { success: (data) => {
this.divExtras.data('language-calcular', data.language.calcular);
this.#loadExtrasData(data.servicios_extra); this.#loadExtrasData(data.servicios_extra);
this.#changeTab('pills-extras'); this.#changeTab('pills-extras');
}, },
@ -1350,15 +1352,22 @@ class PresupuestoCliente {
if (id === 'btn-prev-extras') { if (id === 'btn-prev-extras') {
this.#changeTab('pills-seleccion-tirada'); this.#changeTab('pills-seleccion-tirada');
this.summaryTableExtras.addClass('d-none');
} else { } else {
//this.#changeTab('pills-finalizar'); //this.#changeTab('pills-finalizar');
} }
}); });
// Eventos para el resumen
$(document).on('change', '.service-checkbox', (e) => {
Summary.updateExtras();
});
} }
#loadExtrasData(servicios) { #loadExtrasData(servicios) {
this.divExtras.empty(); this.divExtras.empty();
this.summaryTableExtras.removeClass('d-none');
this.divExtras.removeClass('animate-fadeInUpBounce'); this.divExtras.removeClass('animate-fadeInUpBounce');
this.divExtras.addClass('animate-fadeInUpBounce'); this.divExtras.addClass('animate-fadeInUpBounce');
@ -1366,6 +1375,8 @@ class PresupuestoCliente {
const item = new ServiceOptionCard(extra); const item = new ServiceOptionCard(extra);
this.divExtras.append(item.render()); this.divExtras.append(item.render());
} }
Summary.updateExtras();
} }

View File

@ -1,9 +1,19 @@
$(document).on('click', '#maquetacion', function (e) { import * as Summary from "./summary.js";
$(document).on('change', '#maquetacion', function (e) {
e.preventDefault(); e.preventDefault();
$.get("/presupuesto/public/maquetacion/form", function (data) { if ($('#maquetacion').is(':checked')) {
$("#maquetacionModalBody").html(data); $.get("/presupuesto/public/maquetacion/form", function (data) {
$("#maquetacionModal").modal("show"); $("#maquetacionModalBody").html(data);
}); $("#maquetacionModal").modal("show");
});
} else {
const calcularStr = $('#div-extras').data('language-calcular');
$('#maquetacion').data('price', calcularStr);
$('label[for="maquetacion"] .service-price')
.text(calcularStr);
$('#maquetacion').prop('checked', false);
}
}); });
$(document).on("submit", "#maquetacionForm", function (e) { $(document).on("submit", "#maquetacionForm", function (e) {
@ -15,15 +25,91 @@ $(document).on("submit", "#maquetacionForm", function (e) {
url: $form.attr("action"), url: $form.attr("action"),
type: $form.attr("method"), type: $form.attr("method"),
data: $form.serialize(), data: $form.serialize(),
success: function (data) { dataType: "json",
// obtener el json devuelto success: function (json) {
const json = JSON.parse(data);
const modalEl = document.getElementById("maquetacionModal");
const modal = bootstrap.Modal.getInstance(modalEl) || new bootstrap.Modal(modalEl);
modal.hide();
const resumenHtml = `
<div class="text-start">
<p>Páginas calculadas: ${json.numPaginasEstimadas ?? "-"}</p>
<p>Precio por página estimado: ${formateaMoneda(json.precioPaginaEstimado) || "-"}</p>
<hr class="my-2">
${json.precio ?
`<h3 class="mb-0"><strong>Precio:</strong> ${formateaMoneda(json.precio)}</h3>` : ""}
</div>
`;
Swal.fire({
title: json.language.presupuesto_maquetacion || 'Presupuesto Maquetación',
html: resumenHtml,
icon: 'info',
showCancelButton: true,
confirmButtonText: json.language.add_to_presupuesto || 'Añadir al presupuesto',
cancelButtonText: json.language.cancel || 'Cancelar',
customClass: {
confirmButton: 'btn btn-secondary me-2', // clases para el botón confirmar
cancelButton: 'btn btn-light' // clases para cancelar
},
buttonsStyling: false,
reverseButtons: false,
allowOutsideClick: false
}).then((result) => {
if (result.isConfirmed) {
$('#maquetacion').prop('checked', true);
$('#maquetacion').data('price', json.precio);
$('label[for="maquetacion"] .service-price')
.text(formateaMoneda(json.precio));
Summary.updateExtras();
}
else {
const calcularStr = $('#div-extras').data('language-calcular');
$('#maquetacion').prop('checked', false);
$('#maquetacion').data('price', calcularStr);
$('label[for="maquetacion"] .service-price').text(calcularStr);
}
});
}, },
error: function (xhr, status, error) { error: function (xhr, status, error) {
$("#maquetacionModalBody").html( try {
"<div class='alert alert-danger'>" + xhr.responseText + "</div>" const errs = JSON.parse(xhr.responseText); // { numCaracteres: "…" , … }
); // limpia errores previos
$form.find(".is-invalid").removeClass("is-invalid");
$form.find(".invalid-feedback").text("");
// recorre los errores y los muestra
Object.entries(errs).forEach(([field, msg]) => {
const $input = $form.find(`[name="${field}"]`);
if ($input.length) {
$input.addClass("is-invalid");
$input.siblings(".invalid-feedback").text(msg);
}
});
} catch {
$("#maquetacionModalBody").html(
"<div class='alert alert-danger'>" + xhr.responseText + "</div>"
);
}
} }
}); });
}); });
$(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);
});
function formateaMoneda(valor) {
try {
return new Intl.NumberFormat('es-ES', { style: 'currency', currency: 'EUR' }).format(valor);
} catch {
return valor;
}
}

View File

@ -138,4 +138,27 @@ export function updateFaja() {
$('#summary-faja-tamanio-solapa').text($('#tamanio-solapas-faja').val() + ' mm'); $('#summary-faja-tamanio-solapa').text($('#tamanio-solapas-faja').val() + ' mm');
$('#summary-faja-acabado').text($('#faja-acabado option:selected').text()); $('#summary-faja-acabado').text($('#faja-acabado option:selected').text());
} }
}
export function updateExtras() {
const $table = $('#summary-servicios-extras');
const $tbody = $table.find('tbody');
$tbody.empty();
// Agregar las filas de servicios extras
$('.service-checkbox:checked').each(function() {
const $servicio = $(this);
const resumen = $(`label[for="${$servicio.attr('id')}"] .service-title`).text().trim() || $servicio.attr('id');
const price = $(`label[for="${$servicio.attr('id')}"] .service-price`).text().trim() || $servicio.attr('price');
const $row = $('<tr>').append(
$('<td>').append($('<span>').text(resumen)),
$('<td class="text-end">').text(price)
);
$tbody.append($row);
});
if ($tbody.children().length > 0) {
$table.removeClass('d-none');
} else {
$table.addClass('d-none');
}
} }

View File

@ -205,6 +205,15 @@
</tr> </tr>
</tbody> </tbody>
</table> </table>
<table id="summary-servicios-extras" class="table table-responsive align-middle mb-0 d-none">
<thead class="table-light">
<tr>
<th th:text="#{presupuesto.extras}" scope="col" colspan="2">Servicios</th>
</tr>
</thead>
<tbody>
</tbody>
</table>
</div> </div>
</div> </div>
<!-- end card body --> <!-- end card body -->

View File

@ -1,8 +1,9 @@
<div th:fragment="maquetacionForm"> <div th:fragment="maquetacionForm">
<form id="maquetacionForm" th:action="@{/presupuesto/public/maquetacion}" th:object="${presupuestoMaquetacion}" method="get"> <form id="maquetacionForm" novalidate th:action="@{/presupuesto/public/maquetacion}" th:object="${presupuestoMaquetacion}" method="get">
<div class="form-group"> <div class="form-group">
<label th:text="#{presupuesto.maquetacion.num-caracteres}" for="num-caracteres">Número de caracteres</label> <label th:text="#{presupuesto.maquetacion.num-caracteres}" for="num-caracteres">Número de caracteres</label>
<input type="number" class="form-control" id="num-caracteres" th:field="*{numCaracteres}" min="1" required> <input type="number" class="form-control" id="num-caracteres" th:field="*{numCaracteres}" min="1" required>
<div class="invalid-feedback"></div>
<label th:text="#{presupuesto.maquetacion.num-caracteres-descripcion}" class="form-text text-muted"></label> <label th:text="#{presupuesto.maquetacion.num-caracteres-descripcion}" class="form-text text-muted"></label>
</div> </div>
<div class="form-group"> <div class="form-group">
@ -27,15 +28,18 @@
<div class="form-group"> <div class="form-group">
<label th:text="#{presupuesto.maquetacion.num-columnas}" for="num-columnas">Número de columnas</label> <label th:text="#{presupuesto.maquetacion.num-columnas}" for="num-columnas">Número de columnas</label>
<input type="number" class="form-control" id="num-columnas" th:field="*{numColumnas}" min="1" required> <input type="number" class="form-control" id="num-columnas" th:field="*{numColumnas}" min="1" required>
<div class="invalid-feedback"></div>
<label th:text="#{presupuesto.maquetacion.num-columnas-descripcion}" class="form-text text-muted"></label> <label th:text="#{presupuesto.maquetacion.num-columnas-descripcion}" class="form-text text-muted"></label>
</div> </div>
<div class="form-group"> <div class="form-group">
<label th:text="#{presupuesto.maquetacion.num-tablas}" for="num-tablas">Número de tablas</label> <label th:text="#{presupuesto.maquetacion.num-tablas}" for="num-tablas">Número de tablas</label>
<input type="number" class="form-control" id="num-tablas" th:field="*{numTablas}" min="0" required> <input type="number" class="form-control" id="num-tablas" th:field="*{numTablas}" min="0" required>
<div class="invalid-feedback"></div>
</div> </div>
<div class="form-group"> <div class="form-group">
<label th:text="#{presupuesto.maquetacion.num-fotos}" for="num-fotos">Número de fotos</label> <label th:text="#{presupuesto.maquetacion.num-fotos}" for="num-fotos">Número de fotos</label>
<input type="number" class="form-control" id="num-fotos" th:field="*{numFotos}" min="0" required> <input type="number" class="form-control" id="num-fotos" th:field="*{numFotos}" min="0" required>
<div class="invalid-feedback"></div>
</div> </div>
<div class="form-check form-switch form-switch-custom mb-3 mt-3"> <div class="form-check form-switch form-switch-custom mb-3 mt-3">
<input type="checkbox" class="form-check-input form-switch-custom-primary" <input type="checkbox" class="form-check-input form-switch-custom-primary"
@ -46,7 +50,7 @@
<div class="form-check form-switch form-switch-custom mb-3"> <div class="form-check form-switch form-switch-custom mb-3">
<input type="checkbox" class="form-check-input form-switch-custom-primary" <input type="checkbox" class="form-check-input form-switch-custom-primary"
id="texto-mecanografiado" name="texto-mecanografiado" th:field="*{textoMecanografiado}"> id="texto-mecanografiado" name="texto-mecanografiado" th:field="*{textoMecanografiado}">
<label for="texto-mecanografiado" class="form-check-label" th:text="#{presupuesto.formato-personalizado}"> <label for="texto-mecanografiado" class="form-check-label" th:text="#{presupuesto.maquetacion.texto-mecanografiado}">
Texto mecanografiado Texto mecanografiado
</label> </label>
</div> </div>