mirror of
https://git.imnavajas.es/jjimenez/erp-imprimelibros.git
synced 2026-01-19 15:20:20 +00:00
terminado
This commit is contained in:
@ -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
|
||||
@ -1,3 +1,9 @@
|
||||
app.currency-symbol=€
|
||||
app.yes=Sí
|
||||
app.no=No
|
||||
app.no=No
|
||||
app.aceptar=Aceptar
|
||||
app.cancelar=Cancelar
|
||||
app.guardar=Guardar
|
||||
app.editar=Editar
|
||||
app.eliminar=Eliminar
|
||||
app.imprimir=Imprimir
|
||||
@ -3,6 +3,7 @@ presupuesto.interior=Interior
|
||||
presupuesto.cubierta=Cubierta
|
||||
presupuesto.seleccion-tirada=Seleccion de tirada
|
||||
presupuesto.extras=Extras
|
||||
presupuesto.add-to-presupuesto=Añadir al presupuesto
|
||||
|
||||
# Pestaña datos generales de presupuesto
|
||||
presupuesto.informacion-libro=Información del libro
|
||||
@ -148,6 +149,7 @@ 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
|
||||
|
||||
# Resumen del presupuesto
|
||||
presupuesto.resumen-presupuesto=Resumen presupuesto
|
||||
@ -160,6 +162,7 @@ presupuesto.paginas=Páginas
|
||||
presupuesto.solapas=Solapas
|
||||
presupuesto.papel-gramaje=Papel y gramaje
|
||||
|
||||
# Presupuesto de maquetación
|
||||
presupuesto.maquetacion=Presupuesto de maquetación
|
||||
presupuesto.maquetacion.num-caracteres=Número de caracteres
|
||||
presupuesto.maquetacion.num-caracteres-descripcion=Caracteres con espacios (obtenidos desde Word)
|
||||
|
||||
6
src/main/resources/i18n/validation_en.properties
Normal file
6
src/main/resources/i18n/validation_en.properties
Normal 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
|
||||
8
src/main/resources/i18n/validation_es.properties
Normal file
8
src/main/resources/i18n/validation_es.properties
Normal 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
|
||||
|
||||
|
||||
@ -1,37 +1,54 @@
|
||||
(function () {
|
||||
"use strict";
|
||||
"use strict";
|
||||
|
||||
const default_lang = "es";
|
||||
const language = localStorage.getItem("language");
|
||||
const DEFAULT_LANG = "es";
|
||||
|
||||
function initLanguage() {
|
||||
const saved = localStorage.getItem("language") || default_lang;
|
||||
setLanguage(saved, false); // solo actualiza bandera y lang
|
||||
document.querySelectorAll('.language').forEach(a => {
|
||||
a.addEventListener('click', () => setLanguage(a.dataset.lang, true));
|
||||
});
|
||||
}
|
||||
function getCurrentLang() {
|
||||
// Viene del servidor (Thymeleaf): <html th:lang="${#locale.language}">
|
||||
return document.documentElement.lang || DEFAULT_LANG;
|
||||
}
|
||||
|
||||
function setLanguage(lang, redirect = true) {
|
||||
const already = document.documentElement.lang === lang;
|
||||
function setFlag(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
|
||||
document.documentElement.lang = lang;
|
||||
document.getElementById("header-lang-img").src =
|
||||
lang === "en" ? "/assets/images/flags/gb.svg"
|
||||
: "/assets/images/flags/spain.svg";
|
||||
localStorage.setItem("language", lang);
|
||||
function onLanguageClick(e) {
|
||||
e.preventDefault();
|
||||
const lang = this.dataset.lang;
|
||||
if (!lang || lang === getCurrentLang()) return;
|
||||
|
||||
// Redirige si cambia el idioma
|
||||
if (!already && redirect) {
|
||||
const url = new URL(location.href);
|
||||
url.searchParams.set("lang", lang);
|
||||
location.href = url.toString();
|
||||
}
|
||||
}
|
||||
// Guarda la preferencia (opcional)
|
||||
try { localStorage.setItem("language", lang); } catch {}
|
||||
|
||||
// Llama al inicializador de idioma en cuanto el DOM esté listo
|
||||
document.addEventListener("DOMContentLoaded", function () {
|
||||
initLanguage();
|
||||
});
|
||||
// Redirige con ?lang=... para que Spring cambie el Locale y renderice en ese idioma
|
||||
const url = new URL(location.href);
|
||||
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);
|
||||
})();
|
||||
|
||||
@ -129,6 +129,7 @@ class PresupuestoCliente {
|
||||
this.summaryTableCubierta = $('#summary-cubierta');
|
||||
this.summaryTableSobrecubierta = $('#summary-sobrecubierta');
|
||||
this.summaryTableFaja = $('#summary-faja');
|
||||
this.summaryTableExtras = $('#summary-servicios-extras');
|
||||
}
|
||||
|
||||
init() {
|
||||
@ -1280,6 +1281,7 @@ class PresupuestoCliente {
|
||||
type: 'POST',
|
||||
data: data,
|
||||
success: (data) => {
|
||||
this.divExtras.data('language-calcular', data.language.calcular);
|
||||
this.#loadExtrasData(data.servicios_extra);
|
||||
this.#changeTab('pills-extras');
|
||||
},
|
||||
@ -1350,15 +1352,22 @@ class PresupuestoCliente {
|
||||
|
||||
if (id === 'btn-prev-extras') {
|
||||
this.#changeTab('pills-seleccion-tirada');
|
||||
this.summaryTableExtras.addClass('d-none');
|
||||
} else {
|
||||
//this.#changeTab('pills-finalizar');
|
||||
}
|
||||
});
|
||||
|
||||
// Eventos para el resumen
|
||||
$(document).on('change', '.service-checkbox', (e) => {
|
||||
Summary.updateExtras();
|
||||
});
|
||||
}
|
||||
|
||||
#loadExtrasData(servicios) {
|
||||
|
||||
this.divExtras.empty();
|
||||
this.summaryTableExtras.removeClass('d-none');
|
||||
this.divExtras.removeClass('animate-fadeInUpBounce');
|
||||
|
||||
this.divExtras.addClass('animate-fadeInUpBounce');
|
||||
@ -1366,6 +1375,8 @@ class PresupuestoCliente {
|
||||
const item = new ServiceOptionCard(extra);
|
||||
this.divExtras.append(item.render());
|
||||
}
|
||||
|
||||
Summary.updateExtras();
|
||||
}
|
||||
|
||||
|
||||
|
||||
@ -1,9 +1,19 @@
|
||||
$(document).on('click', '#maquetacion', function (e) {
|
||||
import * as Summary from "./summary.js";
|
||||
|
||||
$(document).on('change', '#maquetacion', function (e) {
|
||||
e.preventDefault();
|
||||
$.get("/presupuesto/public/maquetacion/form", function (data) {
|
||||
$("#maquetacionModalBody").html(data);
|
||||
$("#maquetacionModal").modal("show");
|
||||
});
|
||||
if ($('#maquetacion').is(':checked')) {
|
||||
$.get("/presupuesto/public/maquetacion/form", function (data) {
|
||||
$("#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) {
|
||||
@ -15,15 +25,91 @@ $(document).on("submit", "#maquetacionForm", function (e) {
|
||||
url: $form.attr("action"),
|
||||
type: $form.attr("method"),
|
||||
data: $form.serialize(),
|
||||
success: function (data) {
|
||||
// obtener el json devuelto
|
||||
const json = JSON.parse(data);
|
||||
|
||||
dataType: "json",
|
||||
success: function (json) {
|
||||
|
||||
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) {
|
||||
$("#maquetacionModalBody").html(
|
||||
"<div class='alert alert-danger'>" + xhr.responseText + "</div>"
|
||||
);
|
||||
try {
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -138,4 +138,27 @@ export function updateFaja() {
|
||||
$('#summary-faja-tamanio-solapa').text($('#tamanio-solapas-faja').val() + ' mm');
|
||||
$('#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');
|
||||
}
|
||||
}
|
||||
@ -205,6 +205,15 @@
|
||||
</tr>
|
||||
</tbody>
|
||||
</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>
|
||||
<!-- end card body -->
|
||||
|
||||
@ -1,8 +1,9 @@
|
||||
<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">
|
||||
<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>
|
||||
<div class="invalid-feedback"></div>
|
||||
<label th:text="#{presupuesto.maquetacion.num-caracteres-descripcion}" class="form-text text-muted"></label>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
@ -27,15 +28,18 @@
|
||||
<div class="form-group">
|
||||
<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>
|
||||
<div class="invalid-feedback"></div>
|
||||
<label th:text="#{presupuesto.maquetacion.num-columnas-descripcion}" class="form-text text-muted"></label>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<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>
|
||||
<div class="invalid-feedback"></div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<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>
|
||||
<div class="invalid-feedback"></div>
|
||||
</div>
|
||||
<div class="form-check form-switch form-switch-custom mb-3 mt-3">
|
||||
<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">
|
||||
<input type="checkbox" class="form-check-input form-switch-custom-primary"
|
||||
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
|
||||
</label>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user