trabajando en el delete

This commit is contained in:
2025-10-10 13:28:03 +02:00
parent 6c4b63daa6
commit d4d83fe118
11 changed files with 221 additions and 55 deletions

View File

@ -46,6 +46,11 @@ public abstract class AbstractAuditedEntity {
@Column(name = "deleted_at")
private Instant deletedAt;
@LastModifiedBy
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "deleted_by")
private User deletedBy;
// Getters/Setters
public Long getId() { return id; }
public void setId(Long id) { this.id = id; }
@ -67,4 +72,7 @@ public abstract class AbstractAuditedEntity {
public Instant getDeletedAt() { return deletedAt; }
public void setDeletedAt(Instant deletedAt) { this.deletedAt = deletedAt; }
public User getDeletedBy() { return deletedBy; }
public void setDeletedBy(User deletedBy) { this.deletedBy = deletedBy; }
}

View File

@ -320,8 +320,6 @@ public class MargenPresupuestoController {
return repo.findById(id).map(u -> {
try {
u.setDeleted(true);
u.setDeletedAt(LocalDateTime.now());

View File

@ -1,7 +1,11 @@
package com.imprimelibros.erp.presupuesto;
import org.springframework.stereotype.Controller;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.ui.Model;
import java.time.Instant;
import java.time.LocalDateTime;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;
@ -21,12 +25,15 @@ import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import org.springframework.web.servlet.mvc.support.RedirectAttributes;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.web.server.ResponseStatusException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.core.type.TypeReference;
@ -40,6 +47,9 @@ import com.imprimelibros.erp.presupuesto.classes.PresupuestoMarcapaginas;
import com.imprimelibros.erp.presupuesto.dto.Presupuesto;
import com.imprimelibros.erp.presupuesto.service.PresupuestoService;
import com.imprimelibros.erp.presupuesto.validation.PresupuestoValidationGroups;
import com.imprimelibros.erp.users.UserDao;
import com.imprimelibros.erp.users.User;
import com.imprimelibros.erp.users.UserDetailsImpl;
import com.imprimelibros.erp.presupuesto.service.PresupuestoFormDataMapper;
import com.imprimelibros.erp.presupuesto.service.PresupuestoFormDataMapper.PresupuestoFormDataDto;
@ -66,16 +76,18 @@ public class PresupuestoController {
private final PresupuestoDatatableService dtService;
private final VariableService variableService;
private final PresupuestoFormDataMapper formDataMapper;
private final UserDao userRepo;
public PresupuestoController(ObjectMapper objectMapper, TranslationService translationService,
PresupuestoDatatableService dtService, PresupuestoRepository presupuestoRepository,
VariableService variableService, PresupuestoFormDataMapper formDataMapper) {
VariableService variableService, PresupuestoFormDataMapper formDataMapper, UserDao userRepo) {
this.objectMapper = objectMapper;
this.translationService = translationService;
this.dtService = dtService;
this.presupuestoRepository = presupuestoRepository;
this.variableService = variableService;
this.formDataMapper = formDataMapper;
this.userRepo = userRepo;
}
@PostMapping("/public/validar/datos-generales")
@ -122,7 +134,6 @@ public class PresupuestoController {
errores.put(error.getField(), msg);
});
// errores globales (@ConsistentTiradas...)
result.getGlobalErrors().forEach(error -> errores.put("global", error.getDefaultMessage()));
@ -151,7 +162,6 @@ public class PresupuestoController {
errores.put(error.getField(), msg);
});
// errores globales (@ConsistentTiradas...)
result.getGlobalErrors().forEach(error -> errores.put("global", error.getDefaultMessage()));
@ -187,7 +197,6 @@ public class PresupuestoController {
errores.put(error.getField(), msg);
});
// errores globales (@ConsistentTiradas...)
result.getGlobalErrors().forEach(error -> errores.put("global", error.getDefaultMessage()));
@ -219,7 +228,6 @@ public class PresupuestoController {
errores.put(error.getField(), msg);
});
if (!errores.isEmpty()) {
return ResponseEntity.badRequest().body(errores);
}
@ -271,7 +279,6 @@ public class PresupuestoController {
errores.put(error.getField(), msg);
});
if (!errores.isEmpty()) {
return ResponseEntity.badRequest().body(errores);
}
@ -305,7 +312,6 @@ public class PresupuestoController {
errores.put(error.getField(), msg);
});
if (!errores.isEmpty()) {
return ResponseEntity.badRequest().body(errores);
}
@ -484,7 +490,15 @@ public class PresupuestoController {
@GetMapping
public String getPresupuestoList(Model model, Authentication authentication, Locale locale) {
List<String> keys = List.of();
List<String> keys = List.of(
"presupuesto.delete.title",
"presupuesto.delete.text",
"presupuesto.eliminar",
"presupuesto.delete.button",
"app.yes",
"app.cancelar",
"presupuesto.delete.ok.title",
"presupuesto.delete.ok.text");
Map<String, String> translations = translationService.getTranslations(locale, keys);
model.addAttribute("languageBundle", translations);
@ -570,4 +584,55 @@ public class PresupuestoController {
return dtService.datatableAnonimos(dt, locale);
}
@DeleteMapping("/{id}")
@Transactional
public ResponseEntity<?> delete(@PathVariable Long id, Authentication auth, Locale locale) {
Presupuesto p = presupuestoRepository.findById(id)
.orElseThrow(() -> new ResponseStatusException(
HttpStatus.NOT_FOUND,
messageSource.getMessage("presupuesto.error.not-found", null, locale)));
boolean isUser = auth != null && auth.getAuthorities().stream()
.anyMatch(a -> a.getAuthority().equals("ROLE_USER"));
// compara por IDs (no uses equals entre tipos distintos)
Long ownerId = p.getUser() != null ? p.getUser().getId() : null;
User currentUser = null;
Long currentUserId = null;
if (auth != null && auth.getPrincipal() instanceof UserDetailsImpl udi) {
currentUserId = udi.getId();
currentUser = userRepo.findById(currentUserId).orElse(null);
} else if (auth != null) {
currentUserId = userRepo.findIdByUserNameIgnoreCase(auth.getName()).orElse(null); // fallback
currentUser = userRepo.findById(currentUserId).orElse(null);
}
boolean isOwner = ownerId != null && ownerId.equals(currentUserId);
if (isUser && !isOwner) {
throw new ResponseStatusException(
HttpStatus.FORBIDDEN,
messageSource.getMessage("presupuesto.error.delete-permission-denied", null, locale));
}
if (p.getEstado() != null && !p.getEstado().equals(Presupuesto.Estado.borrador)) {
throw new ResponseStatusException(
HttpStatus.FORBIDDEN,
messageSource.getMessage("presupuesto.error.delete-not-draft", null, locale));
}
// SOFT DELETE (no uses deleteById)
p.setDeleted(true);
p.setDeletedAt(Instant.now());
p.setDeletedBy(currentUser);
presupuestoRepository.save(p);
return ResponseEntity.ok(Map.of("message",
messageSource.getMessage("presupuesto.exito.eliminado", null, locale)));
}
}

View File

@ -174,6 +174,7 @@ public class PresupuestoDatatableService {
m.put("region", p.getRegion());
m.put("ciudad", p.getCiudad());
m.put("updatedAt", formatDate(p.getUpdatedAt(), locale));
if(p.getEstado().equals(Presupuesto.Estado.borrador)){
m.put("actions",
"<div class=\"hstack gap-3 flex-wrap\">" +
"<a href=\"javascript:void(0);\" data-id=\"" + p.getId()
@ -182,6 +183,14 @@ public class PresupuestoDatatableService {
+ "\" class=\"link-danger btn-delete-anonimo fs-15\"><i class=\"ri-delete-bin-5-line\"></i></a>"
+
"</div>");
}
else{
m.put("actions",
"<div class=\"hstack gap-3 flex-wrap\">" +
"<a href=\"javascript:void(0);\" data-id=\"" + p.getId()
+ "\" class=\"link-success btn-edit-anonimo fs-15\"><i class=\"ri-eye-line\"></i></a>" +
"</div>");
}
return m;
}

View File

@ -1,5 +1,6 @@
presupuesto.title=Presupuestos
presupuesto.editar.title=Editar presupuesto
presupuesto.eliminar=Eliminar presupuesto
presupuesto.datos-generales=Datos Generales
presupuesto.interior=Interior
presupuesto.cubierta=Cubierta
@ -265,6 +266,18 @@ presupuesto.marcapaginas.acabado.plastificado-mate-2c=Plastificado mate 2/C
presupuesto.marcapaginas.precio-unidad=Precio por unidad
presupuesto.marcapaginas.precio-total=Precio total
# Mensajes de eliminación de presupuesto
presupuesto.delete.title=Eliminar presupuesto
presupuesto.delete.button=Si, ELIMINAR
presupuesto.delete.text=¿Está seguro de que desea eliminar este presupuesto?<br>Esta acción no se puede deshacer.
presupuesto.delete.ok.title=Presupuesto eliminado
presupuesto.delete.ok.text=El presupuesto ha sido eliminado con éxito.
presupuesto.exito.eliminado=Presupuesto eliminado con éxito.
presupuesto.error.delete-internal-error=No se puede eliminar: error interno.
presupuesto.error.delete-permission-denied=No se puede eliminar: permiso denegado.
presupuesto.error.delete-not-found=No se puede eliminar: presupuesto no encontrado.
presupuesto.error.delete-not-draft=Solo se pueden eliminar presupuestos en estado Borrador.
# Errores
presupuesto.errores-title=Corrija los siguientes errores:
presupuesto.errores.titulo=El título es obligatorio
@ -291,4 +304,6 @@ presupuesto.errores.acabado-cubierta=Seleccione el acabado de la cubierta
presupuesto.errores.presupuesto-no-existe=El presupuesto con ID {0} no existe.
presupuesto.errores.presupuesto-maquetacion=No se pudo calcular el presupuesto de maquetación.
presupuesto.errores.presupuesto-marcapaginas=No se pudo calcular el presupuesto de marcapáginas.
presupuesto.errores.presupuesto-marcapaginas=No se pudo calcular el presupuesto de marcapáginas.
presupuesto.info.presupuestos-anonimos-view=Estos presupuestos son anónimos y no están asociados a ningún cliente. No se guardarán los cambios (sólo consulta).

View File

@ -1,20 +1,24 @@
$('.imagen-container-group').on('click', '.image-container', function (e) {
e.preventDefault(); // <- evita que el label dispare el cambio nativo (2º change)
e.stopPropagation();
$('.imagen-container-group').on('click', '.image-container', function () {
const clicked = $(this);
const group = clicked.closest('.imagen-container-group');
// Limpiar selección anterior
// Si ya está seleccionado, no hagas nada
const $radio = clicked.find('input[type="radio"]');
if ($radio.prop('checked')) return;
// Limpiar selección anterior (solo clases/animación)
group.find('.image-container').removeClass('selected')
.find('.image-presupuesto').removeClass('zoom-anim');
// Marcar nuevo seleccionado
// Marcar nuevo seleccionado (clases/animación)
clicked.addClass('selected');
// Aplicar animación de zoom
const img = clicked.find('.image-presupuesto');
void img[0].offsetWidth; // Forzar reflow
void img[0].offsetWidth;
img.addClass('zoom-anim');
clicked.find('input[type="radio"]').prop('checked', true).trigger('change');
// Marca el radio y dispara UN único change
$radio.prop('checked', true).trigger('change');
});

View File

@ -173,6 +173,9 @@ export default class PresupuestoWizard {
this.summaryTableSobrecubierta = $('#summary-sobrecubierta');
this.summaryTableFaja = $('#summary-faja');
this.summaryTableExtras = $('#summary-servicios-extras');
// variable para evitar disparar eventos al cargar datos
this._hydrating = false;
}
async init() {
@ -181,9 +184,10 @@ export default class PresupuestoWizard {
const mode = root?.dataset.mode || 'public';
const presupuestoId = root?.dataset.id || null;
this.opts = { mode, presupuestoId };
const stored = sessionStorage.getItem("formData");
let stored = null;
if(this.opts.useSessionCache) {
stored = sessionStorage.getItem("formData");
}
this.#initDatosGenerales();
@ -261,18 +265,11 @@ export default class PresupuestoWizard {
});
}
// Limpiar el sessionStorage al salir de la página
window.addEventListener('beforeunload', () => {
sessionStorage.removeItem('formData');
});
}
#cacheFormData() {
if (!this.opts.useSessionCache) return;
const key = this.opts.mode === 'edit' && this.opts.presupuestoId
? `formData:edit:${this.opts.presupuestoId}`
: `formData:${this.opts.mode}`;
sessionStorage.setItem(key, JSON.stringify(this.formData));
sessionStorage.setItem('formData', JSON.stringify(this.formData));
}
#changeTab(idContenidoTab) {
@ -382,8 +379,9 @@ export default class PresupuestoWizard {
// Eventos para el resumen
$(document).on('change', 'input[name="tipoEncuadernacion"]', (e) => {
if ($(e.target).is(':checked')) {
// Actualizar el resumen
Summary.updateEncuadernacion();
}
});
@ -474,7 +472,7 @@ export default class PresupuestoWizard {
paginasNegro: this.paginasNegro.val(),
paginasColor: this.paginasColor.val(),
posicionPaginasColor: this.posicionPaginasColor.val(),
tipoEncuadernacion: ($('.tipo-libro.selected').length > 0) ? $('.tipo-libro.selected').attr('id') : 'fresado',
tipoEncuadernacion: $('.tipo-libro input:checked').val() || 'fresado',
};
}
@ -519,7 +517,6 @@ export default class PresupuestoWizard {
this.alto.val(this.formData.datosGenerales.alto);
}
$('.tipo-libro').removeClass('selected');
$('input[name="tipoEncuadernacion"][value="' + this.formData.datosGenerales.tipoEncuadernacion + '"]')
.prop('checked', true);
this.#updateTipoEncuadernacion();
@ -565,7 +562,7 @@ export default class PresupuestoWizard {
#updateTipoEncuadernacion() {
const paginas = parseInt($('#paginas').val());
const selectedTipo = $('.tipo-libro.selected').attr('id');
const selectedTipo = $('.tipo-libro input:checked').val();
$('.tipo-libro').removeClass('selected');
if (paginas < 32) {
@ -591,19 +588,17 @@ export default class PresupuestoWizard {
$('.tipo-libro#grapado').removeClass('d-none');
}
if (selectedTipo && $('.tipo-libro#' + selectedTipo).length > 0 && !$('.tipo-libro#' + selectedTipo).hasClass('d-none')) {
$('.tipo-libro#' + selectedTipo).addClass('selected');
}
else {
if (!(selectedTipo && $('.tipo-libro#' + selectedTipo).length > 0 && !$('.tipo-libro#' + selectedTipo).hasClass('d-none'))) {
let firstVisible = $('.tipo-libro').not('.d-none').first();
if (firstVisible.length) {
firstVisible.addClass('selected');
firstVisible.trigger('click');
}
}
if ($('.tipo-libro.selected').length > 0) {
this.formData.datosGenerales.tipoEncuadernacion = $('.tipo-libro.selected').attr('id');
if ($('.tipo-libro input:checked').length > 0) {
this.formData.datosGenerales.tipoEncuadernacion = $('.tipo-libro input:checked').val();
Summary.updateEncuadernacion();
}
else {
@ -629,6 +624,8 @@ export default class PresupuestoWizard {
$(document).on('change', 'input[name="tipoImpresion"]', (e) => {
if (!$(e.target).is(':checked'))
return;
const data = this.#getPresupuestoData();
Summary.updateTipoImpresion();
@ -681,6 +678,10 @@ export default class PresupuestoWizard {
$(document).on('change', 'input[name="papelInterior"]', (e) => {
if (!$(e.target).is(':checked'))
return;
const data = this.#getPresupuestoData();
Summary.updatePapelInterior();
@ -951,6 +952,12 @@ export default class PresupuestoWizard {
$(document).on('change', 'input[name="tipoCubierta"]', (e) => {
if (!$(e.target).is(':checked'))
return;
if(this._hydrating)
return;
$('.tapa-dura-options').eq(0).removeClass('animate-fadeInUpBounce');
$('.tapa-blanda-options').eq(0).removeClass('animate-fadeInUpBounce');
@ -971,6 +978,9 @@ export default class PresupuestoWizard {
$(document).on('change', 'input[name="solapasCubierta"]', (e) => {
if (!$(e.target).is(':checked'))
return;
if (e.currentTarget.closest('.image-container').id === 'sin-solapas') {
this.divSolapasCubierta.addClass('d-none');
}
@ -986,7 +996,7 @@ export default class PresupuestoWizard {
});
$(document).on('click', '.papel-cubierta', (e) => {
$(document).on('change', 'input[name="papel-cubierta"]', (e) => {
const data = this.#getPresupuestoData();
@ -1026,6 +1036,9 @@ export default class PresupuestoWizard {
$(document).on('change', '.datos-cubierta', (e) => {
if(this._hydrating)
return;
const dataToStore = this.#getCubiertaData();
this.#updateCubiertaData(dataToStore);
this.#cacheFormData();
@ -1165,13 +1178,13 @@ export default class PresupuestoWizard {
if (item.extraData["sk-id"] == this.formData.cubierta.papelCubiertaId) {
item.setSelected(true);
}
item.group='papel-cubierta';
this.divPapelCubierta.append(item.render());
}
if (this.divPapelCubierta.find('.image-container.selected').length === 0) {
this.divPapelCubierta.find('.image-container').first().addClass('selected');
this.formData.cubierta.papelCubiertaId =
this.divPapelCubierta.find('.image-container').first().data('sk-id') || 3;
if (this.divPapelCubierta.find('input[name="papel-cubierta"]:checked').length === 0) {
this.divPapelCubierta.find('input[name="papel-cubierta"]').first().prop('checked', true).trigger('change');
}
this.#addGramajesCubierta(data.opciones_gramaje_cubierta);
@ -1192,13 +1205,13 @@ export default class PresupuestoWizard {
#getCubiertaData() {
const tipoCubierta = $('.tapa-cubierta.selected').attr('id') || 'tapaBlanda';
const solapas = $('.solapas-cubierta.selected').id == 'sin-solapas' ? 0 : 1 || 0;
const tipoCubierta = $('.tapa-cubierta input:checked').val() || 'tapaBlanda';
const solapas = $('.solapas-cubierta input:checked').val() == 'sin-solapas' ? 0 : 1 || 0;
const tamanioSolapasCubierta = $('#tamanio-solapas-cubierta').val() || '80';
const cubiertaCaras = parseInt(this.carasImpresionCubierta.val()) || 2;
const papelGuardasId = parseInt($('#papel-guardas option:selected').data('papel-id')) || 3;
const gramajeGuardas = parseInt($('#papel-guardas option:selected').data('gramaje')) || 170;
const guardasImpresas = parseInt(this.guardasImpresas) || 0;
const guardasImpresas = parseInt(this.guardasImpresas.val()) || 0;
const cabezada = this.cabezada.val() || 'WHI';
const papelCubiertaId = $('#div-papel-cubierta .image-container input:checked').parent().data('sk-id') || this.formData.cubierta.papelCubiertaId || 3;
const gramajeCubierta = $('input[name="gramaje-cubierta"]:checked').data('gramaje') || this.formData.cubierta.gramajeCubierta || 170;
@ -1279,7 +1292,7 @@ export default class PresupuestoWizard {
for (let i = 0; i < gramajes.length; i++) {
const gramaje = gramajes[i];
this.#addGramaje(this.divGramajeCubierta, 'gramaje-cubierta datos-cubierta', gramaje, 'gramaje-cubierta');
this.#addGramaje(this.divGramajeCubierta, 'gramaje-cubierta', gramaje, 'gramaje-cubierta');
// Seleccionar el gramaje por defecto
if (this.formData.cubierta.gramajeCubierta === '' && i === 0) {
@ -1298,9 +1311,11 @@ export default class PresupuestoWizard {
#loadCubiertaData() {
this._hydrating = true;
$('input[name="tipoCubierta"][value="' + this.formData.cubierta.tipoCubierta + '"]')
.prop('checked', true);
if (this.formData.cubierta.tipoCubierta === 'tapaBlanda') {
$('.tapa-blanda-options').removeClass('d-none');
$('.tapa-dura-options').addClass('d-none');
@ -1315,15 +1330,16 @@ export default class PresupuestoWizard {
this.cabezada.val(this.formData.cubierta.cabezada);
}
this._hydrating = false;
$('input[name="tipoCubierta"][value="' + this.formData.cubierta.tipoCubierta + '"]').trigger('change');
if (this.formData.cubierta.solapasCubierta === 0) {
$('.solapas-cubierta#sin-solapas').addClass('selected');
$('.solapas-cubierta#sin-solapas input').prop('checked', true);
this.divSolapasCubierta.addClass('d-none');
}
else {
$('.solapas-cubierta').removeClass('selected');
$(`.solapas-cubierta#con-solapas`).addClass('selected');
$('.solapas-cubierta#con-solapas input').prop('checked', true);
this.divSolapasCubierta.removeClass('d-none');
this.carasImpresionCubierta.val(this.formData.cubierta.cubiertaCaras);
this.tamanioSolapasCubierta.val(this.formData.cubierta.tamanioSolapasCubierta);

View File

@ -67,6 +67,7 @@
});
$('#presupuestos-anonimos-datatable').on('click', '.btn-edit-anonimo', function (e) {
e.preventDefault();
const id = $(this).data('id');
if (id) {
@ -74,5 +75,48 @@
}
});
$('#presupuestos-anonimos-datatable').on('click', '.btn-delete-anonimo', function (e) {
e.preventDefault();
const id = $(this).data('id');
Swal.fire({
title: window.languageBundle.get(['presupuesto.delete.title']) || 'Eliminar presupuesto',
html: window.languageBundle.get(['presupuesto.delete.text']) || 'Esta acción no se puede deshacer.',
icon: 'warning',
showCancelButton: true,
buttonsStyling: false,
customClass: {
confirmButton: 'btn btn-danger w-xs mt-2',
cancelButton: 'btn btn-light w-xs mt-2'
},
confirmButtonText: window.languageBundle.get(['presupuesto.delete.button']) || 'Eliminar',
cancelButtonText: window.languageBundle.get(['app.cancelar']) || 'Cancelar',
}).then((result) => {
if (!result.isConfirmed) return;
$.ajax({
url: '/presupuesto/' + id,
type: 'DELETE',
success: function () {
Swal.fire({
icon: 'success', title: window.languageBundle.get(['presupuesto.delete.ok.title']) || 'Eliminado',
text: window.languageBundle.get(['presupuesto.delete.ok.text']) || 'El presupuesto ha sido eliminado con éxito.',
showConfirmButton: true,
customClass: {
confirmButton: 'btn btn-secondary w-xs mt-2',
},
});
$('#presupuestos-anonimos-datatable').DataTable().ajax.reload(null, false);
},
error: function (xhr) {
// 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 });
}
});
});
});
})();

View File

@ -60,7 +60,7 @@
<div th:if="${#authentication.principal.role == 'SUPERADMIN'}">
<li class="nav-item">
<a href="/configuracion/margenes-presupuesto" class="nav-link">
<i class="ri-discount-percent-line"></i>
<i class="ri-percent-line"></i>
<span th:text="#{margenes-presupuesto.titulo}">Márgenes de presupuesto</span>
</a>
</li>

View File

@ -17,6 +17,7 @@
<div class="card-body checkout-tab">
<form action="#">
<input type="hidden" id="cliente_id" th:value="${cliente_id} ?: null" />
<div class="step-arrow-nav mt-n3 mx-n3 mb-3">
<ul class="nav nav-pills nav-justified custom-nav" role="tablist">

View File

@ -37,6 +37,12 @@
<div class="container-fluid">
<!-- alert info -->
<div th:if="${appMode} == 'view'" class="alert alert-warning fade show" role="alert">
<i class="ri-information-fill me-1 align-middle"></i>
<span th:text="#{presupuesto.info.presupuestos-anonimos-view}"></span>
</div>
<div th:insert="~{imprimelibros/presupuestos/presupuestador :: presupuestador}"></div>
</div>
</div>