guardando presupuestos anonimos

This commit is contained in:
2025-10-11 14:14:47 +02:00
parent d4d83fe118
commit a1359f37b0
28 changed files with 697 additions and 232 deletions

View File

@ -0,0 +1,9 @@
import PresupuestoWizard from './wizard.js';
const app = new PresupuestoWizard({
mode: 'public',
readonly: false,
canSave: true,
useSessionCache: false,
});
app.init();

View File

@ -1,7 +1,7 @@
import PresupuestoWizard from './wizard.js';
const app = new PresupuestoWizard({
mode: 'view',
mode: 'public',
readonly: true,
canSave: false,
useSessionCache: false,

View File

@ -22,15 +22,15 @@ export default class PresupuestoWizard {
titulo: '',
autor: '',
isbn: '',
tirada1: '',
tirada1: 10,
tirada2: '',
tirada3: '',
tirada4: '',
ancho: '',
alto: '',
ancho: 148,
alto: 218,
formatoPersonalizado: false,
paginasNegro: '',
paginasColor: '',
paginasNegro: 0,
paginasColor: 32,
posicionPaginasColor: '',
tipoEncuadernacion: 'fresado',
},
@ -99,7 +99,7 @@ export default class PresupuestoWizard {
}
}
},
selectedTirada: null,
selectedTirada: 10,
}
// pestaña datos generales
@ -180,12 +180,20 @@ export default class PresupuestoWizard {
async init() {
$.ajaxSetup({
beforeSend: function (xhr) {
const token = document.querySelector('meta[name="_csrf"]')?.content;
const header = document.querySelector('meta[name="_csrf_header"]')?.content;
if (token && header) xhr.setRequestHeader(header, token);
}
});
const root = document.getElementById('presupuesto-app');
const mode = root?.dataset.mode || 'public';
const presupuestoId = root?.dataset.id || null;
let stored = null;
if(this.opts.useSessionCache) {
if (this.opts.useSessionCache) {
stored = sessionStorage.getItem("formData");
}
@ -238,29 +246,75 @@ export default class PresupuestoWizard {
if (this.opts.canSave) {
$('#btn-guardar').on('click', async () => {
// compón el payload con lo que ya tienes:
const alert = $('#form-errors');
const servicios = [];
$('.service-checkbox:checked').each(function () {
const $servicio = $(this);
servicios.push({
id: $servicio.attr('id') ?? $(`label[for="${$servicio.attr('id')}"] .service-title`).text().trim(),
label: $(`label[for="${$servicio.attr('id')}"] .service-title`).text().trim(),
units: $servicio.attr('id') === 'marcapaginas' ? self.formData.servicios.datosMarcapaginas.marcapaginas_tirada : 1,
price: $servicio.data('price') ?? $(`label[for="${$servicio.attr('id')}"] .service-price`).text().trim().replace(" " + self.divExtras.data('currency'), ''),
});
});
const payload = {
id: this.opts.presupuestoId,
mode: this.opts.mode,
presupuesto: this.#getPresupuestoData(),
servicios: servicios,
};
try {
const res = await fetch(this.opts.endpoints.save, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload)
}).then(r => r.json());
alert.addClass('d-none').find('ul').empty();
$.ajax({
url: '/presupuesto/save',
type: 'POST',
contentType: 'application/json',
data: JSON.stringify(payload)
}).then((data) => {
Swal.fire({
icon: 'success',
title: window.languageBundle?.get('common.guardado') || 'Guardado',
timer: 1800,
buttonsStyling: false,
customClass: {
confirmButton: 'btn btn-secondary me-2', // clases para el botón confirmar
cancelButton: 'btn btn-light' // clases para cancelar
},
showConfirmButton: false
});
// opcional: actualizar window.PRESUPUESTO_ID/resumen con el id devuelto
if (data.id) this.opts.presupuestoId = data.id;
}).catch((xhr, status, error) => {
// feedback
Swal.fire({
icon: 'success',
title: window.languageBundle?.get('common.guardado') || 'Guardado',
timer: 1800,
showConfirmButton: false
const errors = xhr.responseJSON;
if (errors && typeof errors === 'object') {
if (!this.DEBUG && xhr.responseJSON.error && xhr.responseJSON.error == 'Internal Server Error') {
console.error("Error al validar los datos generales. Internal Server Error");
return;
}
Object.values(errors).forEach(errorMsg => {
alert.find('ul').append(`<li>${errorMsg}</li>`);
});
alert.removeClass('d-none');
} else {
alert.find('ul').append('<li>Error desconocido. Por favor, inténtelo de nuevo más tarde.</li>');
$(window).scrollTop(0);
alert.removeClass('d-none');
}
window.scrollTo({ top: 0, behavior: 'smooth' });
});
// opcional: actualizar window.PRESUPUESTO_ID/resumen con el id devuelto
if (res.id) window.PRESUPUESTO_ID = res.id;
} catch (e) {
Swal.fire({ icon: 'error', title: 'Error al guardar', text: e?.message || '' });
Swal.fire({
icon: 'error',
title: 'Error al guardar',
text: e?.message || '',
buttonsStyling: false,
customClass: {
confirmButton: 'btn btn-secondary me-2', // clases para el botón confirmar
cancelButton: 'btn btn-light' // clases para cancelar
},
});
}
});
}
@ -381,7 +435,7 @@ export default class PresupuestoWizard {
$(document).on('change', 'input[name="tipoEncuadernacion"]', (e) => {
if ($(e.target).is(':checked')) {
Summary.updateEncuadernacion();
}
});
@ -507,7 +561,7 @@ export default class PresupuestoWizard {
this.tirada4.val(this.formData.datosGenerales.tirada4);
this.paginasNegro.val(this.formData.datosGenerales.paginasNegro);
this.paginasColor.val(this.formData.datosGenerales.paginasColor);;
this.paginasColor.val(this.formData.datosGenerales.paginasColor);;
this.posicionPaginasColor.val(this.formData.datosGenerales.posicionPaginasColor);
@ -589,7 +643,7 @@ export default class PresupuestoWizard {
}
if (!(selectedTipo && $('.tipo-libro#' + selectedTipo).length > 0 && !$('.tipo-libro#' + selectedTipo).hasClass('d-none'))) {
let firstVisible = $('.tipo-libro').not('.d-none').first();
if (firstVisible.length) {
@ -626,7 +680,7 @@ export default class PresupuestoWizard {
$(document).on('change', 'input[name="tipoImpresion"]', (e) => {
if (!$(e.target).is(':checked'))
return;
const data = this.#getPresupuestoData();
Summary.updateTipoImpresion();
@ -849,7 +903,7 @@ export default class PresupuestoWizard {
for (let i = 0; i < opciones_papel_interior.length; i++) {
const opcion = opciones_papel_interior[i];
const item = new imagen_presupuesto(opcion);
item.group = 'papelInterior';
item.group = 'papelInterior';
item.extraClass = 'interior-data papel-interior';
if (this.formData.interior.papelInteriorId == '' && i === 0 ||
this.formData.interior.papelInteriorId == opcion.extra_data["sk-id"]) {
@ -934,7 +988,12 @@ export default class PresupuestoWizard {
`,
confirmButtonClass: 'btn btn-primary w-xs mt-2',
showConfirmButton: false,
showCloseButton: true
showCloseButton: true,
buttonsStyling: false,
customClass: {
confirmButton: 'btn btn-secondary me-2', // clases para el botón confirmar
cancelButton: 'btn btn-light' // clases para cancelar
},
});
});
@ -946,7 +1005,12 @@ export default class PresupuestoWizard {
html: window.languageBundle.get('presupuesto.impresion-cubierta-help'),
confirmButtonClass: 'btn btn-primary w-xs mt-2',
showConfirmButton: false,
showCloseButton: true
showCloseButton: true,
buttonsStyling: false,
customClass: {
confirmButton: 'btn btn-secondary me-2', // clases para el botón confirmar
cancelButton: 'btn btn-light' // clases para cancelar
},
});
});
@ -955,7 +1019,7 @@ export default class PresupuestoWizard {
if (!$(e.target).is(':checked'))
return;
if(this._hydrating)
if (this._hydrating)
return;
$('.tapa-dura-options').eq(0).removeClass('animate-fadeInUpBounce');
@ -995,7 +1059,7 @@ export default class PresupuestoWizard {
Summary.updateTapaCubierta();
});
$(document).on('change', 'input[name="papel-cubierta"]', (e) => {
const data = this.#getPresupuestoData();
@ -1036,7 +1100,7 @@ export default class PresupuestoWizard {
$(document).on('change', '.datos-cubierta', (e) => {
if(this._hydrating)
if (this._hydrating)
return;
const dataToStore = this.#getCubiertaData();
@ -1178,12 +1242,12 @@ export default class PresupuestoWizard {
if (item.extraData["sk-id"] == this.formData.cubierta.papelCubiertaId) {
item.setSelected(true);
}
item.group='papel-cubierta';
item.group = 'papel-cubierta';
this.divPapelCubierta.append(item.render());
}
if (this.divPapelCubierta.find('input[name="papel-cubierta"]:checked').length === 0) {
this.divPapelCubierta.find('input[name="papel-cubierta"]').first().prop('checked', true).trigger('change');
}
@ -1493,6 +1557,8 @@ export default class PresupuestoWizard {
const body = {
presupuesto: this.#getPresupuestoData(),
save: !this.opts.canSave,
mode: this.opts.mode,
servicios: servicios
};

View File

@ -1,3 +1,5 @@
import { preguntarTipoPresupuesto } from './presupuesto-utils.js';
(() => {
// si jQuery está cargado, añade CSRF a AJAX
const csrfToken = document.querySelector('meta[name="_csrf"]')?.getAttribute('content');
@ -67,7 +69,7 @@
});
$('#presupuestos-anonimos-datatable').on('click', '.btn-edit-anonimo', function (e) {
e.preventDefault();
const id = $(this).data('id');
if (id) {
@ -76,7 +78,7 @@
});
$('#presupuestos-anonimos-datatable').on('click', '.btn-delete-anonimo', function (e) {
e.preventDefault();
const id = $(this).data('id');
@ -113,10 +115,34 @@
// usa el mensaje del backend; fallback genérico por si no llega JSON
const msg = (xhr.responseJSON && xhr.responseJSON.message)
|| 'Error al eliminar el presupuesto.';
Swal.fire({ icon: 'error', title: 'No se pudo eliminar', text: msg });
Swal.fire({
icon: 'error',
title: 'No se pudo eliminar',
text: msg,
buttonsStyling: false,
customClass: {
confirmButton: 'btn btn-secondary me-2', // clases para el botón confirmar
cancelButton: 'btn btn-light' // clases para cancelar
},
});
}
});
});
});
$('#addPresupuestoButton').on('click', async function (e) {
e.preventDefault();
const res = await preguntarTipoPresupuesto();
if (!res) return;
if (res.tipo === 'anonimo') {
console.log('Crear presupuesto ANÓNIMO');
} else {
console.log('Crear presupuesto de CLIENTE:', res.clienteId, res.clienteText);
}
});
})();

View File

@ -0,0 +1,77 @@
/**
* Pregunta el tipo de presupuesto y (si aplica) selecciona cliente.
* Devuelve una promesa con { tipo: 'anonimo'|'cliente', clienteId?: string }
*/
export async function preguntarTipoPresupuesto() {
const { value: tipo } = await Swal.fire({
title: window.languageBundle.get(['presupuesto.add.tipo']) || 'Selecciona tipo de presupuesto',
input: 'radio',
inputOptions: {
anonimo: window.languageBundle.get(['presupuesto.add.anonimo']) || 'Anónimo',
cliente: window.languageBundle.get(['presupuesto.add.cliente']) || 'De cliente'
},
inputValidator: (value) => {
if (!value) {
return window.languageBundle.get(['presupuesto.add.error.options']) || 'Debes seleccionar una opción.';
}
},
confirmButtonText: window.languageBundle.get(['presupuesto.add.next']) || 'Siguiente',
showCancelButton: true,
cancelButtonText: window.languageBundle.get(['presupuesto.add.cancel']) || 'Cancelar',
buttonsStyling: false,
customClass: {
confirmButton: 'btn btn-secondary me-2', // clases para el botón confirmar
cancelButton: 'btn btn-light' // clases para cancelar
},
});
if (!tipo) return null; // Cancelado
if (tipo === 'anonimo') {
return { tipo };
}
// Si es de cliente, mostrar otro paso con Select2
return Swal.fire({
title: window.languageBundle.get(['presupuesto.add.select-client']) || 'Selecciona cliente',
html: `
<select id="selectCliente" class="form-select select2" style="width:100%"></select>
`,
focusConfirm: false,
buttonsStyling: false,
showCancelButton: true,
confirmButtonText: window.languageBundle.get(['presupuesto.add.next']) || 'Aceptar',
cancelButtonText: window.languageBundle.get(['presupuesto.add.cancel']) || 'Cancelar',
customClass: {
confirmButton: 'btn btn-secondary me-2', // clases para el botón confirmar
cancelButton: 'btn btn-light' // clases para cancelar
},
didOpen: () => {
const $select = $('#selectCliente');
// Configura Select2 (AJAX o lista estática)
$select.select2({
dropdownParent: $('.swal2-container'),
ajax: {
url: 'users/api/get-users', // ajusta a tu endpoint
dataType: 'json',
delay: 250,
data: (params) => ({ q: params.term }),
processResults: data => ({
results: data.results,
pagination: data.pagination
}),
cache: true
}
});
},
preConfirm: () => {
const clienteId = $('#selectCliente').val();
const clienteText = $('#selectCliente option:selected').text();
if (!clienteId) {
Swal.showValidationMessage(window.languageBundle.get(['presupuesto.add.error.select-client']) || 'Debes seleccionar un cliente.');
return false;
}
return { tipo, clienteId, clienteText };
}
}).then((r) => (r.isConfirmed ? r.value : null));
}

View File

@ -132,7 +132,16 @@ $(() => {
// usa el mensaje del backend; fallback genérico por si no llega JSON
const msg = (xhr.responseJSON && xhr.responseJSON.message)
|| 'Error al eliminar el usuario.';
Swal.fire({ icon: 'error', title: 'No se pudo eliminar', text: msg });
Swal.fire({
icon: 'error',
title: 'No se pudo eliminar',
text: msg,
buttonsStyling: false,
customClass: {
confirmButton: 'btn btn-secondary me-2', // clases para el botón confirmar
cancelButton: 'btn btn-light' // clases para cancelar
},
});
}
});
});